Wrapping CURL library API using unique_ptr in C++

Aniket Pingley, Ph.D.
Techanic
Published in
5 min readMar 22, 2023

--

CURL is a popular open-source library that provides a simple way to make HTTP requests. It is widely used in C++ applications that require network connectivity. However, working with raw pointers in C++ can be cumbersome and error-prone. To solve this issue, we can use smart pointers such as unique_ptr to manage the lifetime of the objects we create.

In this blog, we will demonstrate how to wrap CURL C functions using unique_ptr for lifetime management in a C++ application. At the end of this blog you can find detailed explanation, usage and benefits of using unique_ptr.

Wrapping CURL C functions

CURL provides a set of C functions that we can use to make HTTP requests. However, working with raw pointers in C++ can be dangerous, especially when dealing with memory allocation and deallocation. To avoid memory leaks and segmentation faults, we can use smart pointers such as unique_ptr to manage the lifetime of the objects we create. Not all functions in the CURL API are covered here, but the example should be enough to get a programmer’s boat rowing.

In this example, we will create a simple wrapper class called CurlRequest that encapsulates the CURL C functions. The CurlRequest class will handle the creation and destruction of the CURL objects using unique_ptr.

#include <curl/curl.h>
#include <memory>
#include <stdexcept>
#include <string>

class CurlRequest {
public:
CurlRequest() {
curl_ = std::unique_ptr<CURL, decltype(&curl_easy_cleanup)>(curl_easy_init(), curl_easy_cleanup);
if (!curl_) {
throw std::runtime_error("Failed to initialize Curl");
}
}

void SetUrl(const std::string& url) {
curl_easy_setopt(curl_.get(), CURLOPT_URL, url.c_str());
}

void SetUserAgent(const std::string& user_agent) {
curl_easy_setopt(curl_.get(), CURLOPT_USERAGENT, user_agent.c_str());
}

void SetTimeout(int timeout_secs) {
curl_easy_setopt(curl_.get(), CURLOPT_TIMEOUT, timeout_secs);
}

void SetFollowLocation(bool follow) {
curl_easy_setopt(curl_.get(), CURLOPT_FOLLOWLOCATION, follow ? 1L : 0L);
}

std::string Execute() {
std::string response;
curl_easy_setopt(curl_.get(), CURLOPT_WRITEFUNCTION, &WriteResponse);
curl_easy_setopt(curl_.get(), CURLOPT_WRITEDATA, &response);
CURLcode res = curl_easy_perform(curl_.get());
if (res != CURLE_OK) {
throw std::runtime_error("Failed to execute Curl request: " + std::string(curl_easy_strerror(res)));
}
return response;
}

private:
std::unique_ptr<CURL, decltype(&curl_easy_cleanup)> curl_;

static size_t WriteResponse(void* ptr, size_t size, size_t nmemb, void* userdata) {
size_t real_size = size * nmemb;
std::string* response = static_cast<std::string*>(userdata);
response->append(static_cast<char*>(ptr), real_size);
return real_size;
}
};


int main() {
try {
CurlRequest request;
request.SetUrl("https://www.example.com");
request.SetUserAgent("Mozilla/5.0");
request.SetTimeout(10);
request.SetFollowLocation(true);
std::string response = request.Execute();
std::cout << response << std::endl;
} catch (std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
return 1;
}
return 0;
}

The CurlRequest class has a default constructor that initializes a unique_ptr to a CURL object using curl_easy_init(). The deleter for the unique_ptr is set to curl_easy_cleanup, which will be called automatically when the object goes out of scope.

This code creates a unique_ptr that manages the lifetime of a CURL object. The unique_ptr is constructed using the curl_easy_init() function to create the underlying CURL object and the curl_easy_cleanup() function as the deleter.

The key piece of code here is the first line of code in the constructor for CurlRequest. Here’s a breakdown of the syntax:

  • std::unique_ptr: This is the class template for unique_ptr, which manages the lifetime of a dynamically allocated object.
  • <CURL, decltype(&curl_easy_cleanup)>: This specifies the type of the object that the unique_ptr will manage (a CURL object) and the type of the deleter that will be used to destroy the object (a function pointer to curl_easy_cleanup()).
  • (curl_easy_init(), curl_easy_cleanup): This constructs the unique_ptr by calling curl_easy_init() to create a new CURL object and passing it to the unique_ptr constructor as the first argument. The second argument is the deleter function, which is a function pointer to curl_easy_cleanup().

In other words, the unique_ptr will take ownership of the CURL object returned by curl_easy_init() and ensure that curl_easy_cleanup() is called when the unique_ptr goes out of scope or is reset. This ensures that the dynamically allocated memory is properly released and helps avoid memory leaks.

About unique pointers

unique_ptr is a smart pointer introduced in C++11 that provides automatic memory management for dynamically allocated objects. It is a class template that represents a unique ownership of a dynamically allocated object.

Unlike raw pointers, unique_ptr is responsible for automatically deallocating the memory allocated for the pointed object when it goes out of scope. This makes it a useful tool for preventing memory leaks and ensuring safe and efficient memory management.

Some of the key benefits of using unique_ptr in C++ are:

  1. Automatic memory management: unique_ptr manages the memory for the object it points to, and automatically deallocates it when the pointer goes out of scope. This eliminates the need for explicit memory management and helps prevent memory leaks.
  2. Single ownership: unique_ptr represents a unique ownership of an object, which means that it cannot be copied or shared. This helps prevent problems such as double deletion of objects.
  3. Type safety: unique_ptr is a type-safe way to manage dynamically allocated objects. It ensures that the pointed object is properly deleted when the unique_ptr goes out of scope or is reset.
  4. Move semantics: unique_ptr can be moved, but not copied. This allows for efficient transfer of ownership between objects and can help avoid unnecessary copies of large or complex objects.

Overall, unique_ptr provides a safe and efficient way to manage dynamically allocated objects in C++, and can help improve code safety and reliability.

Challenges to memory-safety due to raw pointers

When raw pointers are used in C++, there are several challenges related to memory safety that programmers need to be aware of. Here are a few key challenges:

  1. Dangling pointers: A dangling pointer is a pointer that points to an object that has been deleted or deallocated. Accessing the memory pointed to by a dangling pointer can result in undefined behavior, such as a segmentation fault or data corruption.
  2. Memory leaks: A memory leak occurs when memory is allocated but never deallocated. This can lead to a gradual depletion of available memory and eventually cause the program to crash or become unresponsive.
  3. Null pointer dereferencing: Dereferencing a null pointer can result in a segmentation fault or other undefined behavior. It is important to always check if a pointer is null before attempting to access the memory it points to.
  4. Buffer overflows: When using raw pointers to access arrays, it is important to ensure that the pointer does not go out of bounds. A buffer overflow can lead to data corruption, memory corruption, or other security vulnerabilities.

To mitigate these memory safety challenges, it is recommended to use smart pointers or other higher-level abstractions that provide automatic memory management and reduce the need for manual memory allocation and deallocation.

--

--