Usage of std::make_shared in modern C++
C++ is a modern and powerful programming language, which is widely used for developing complex software systems. One of the most important features of modern C++ is the smart pointer, which is a wrapper around a raw pointer that provides automatic memory management. In particular, the std::shared_ptr is a widely used smart pointer that allows multiple objects to share ownership of the same resource.
In this context, the std::make_shared function is a very useful tool for creating and managing shared pointers. This function is used to create a new object and return a shared pointer that owns the object. The main advantage of std::make_shared is that it can improve the performance of your code, by reducing the number of memory allocations and reducing the memory overhead of the shared pointer.
Here are some use cases of std::make_shared
in modern C++:
Creating a new shared pointer:
std::shared_ptr<int> ptr = std::make_shared<int>();
This code creates a new shared pointer that owns a dynamically allocated integer.
Creating a new shared pointer with a value:
std::shared_ptr<int> ptr = std::make_shared<int>(42);
This code creates a new shared pointer that owns a dynamically allocated integer initialized to the value 42.
Creating a new shared pointer to an array:
std::shared_ptr<int[]> ptr = std::make_shared<int[]>(10);
This code creates a new shared pointer that owns a dynamically allocated array of 10 integers.
Creating a new shared pointer to a custom object:
class MyClass {
public:
int value;
};
std::shared_ptr<MyClass> ptr = std::make_shared<MyClass>();
ptr->value = 42;
This code creates a new shared pointer that owns a dynamically allocated object of type MyClass, and sets the value member to 42.
Creating a new shared pointer with a custom deleter:
std::shared_ptr<MyClass> ptr = std::make_shared<MyClass>(
[] (MyClass* obj) {
delete obj;
std::cout << "MyClass object deleted\n";
}
);
This code creates a new shared pointer that owns a dynamically allocated object of type MyClass
, and sets a custom deleter that prints a message when the object is deleted.
In conclusion, std::make_shared
is a powerful tool for creating and managing shared pointers in modern C++. It can improve the performance of your code and simplify the management of dynamic memory. Therefore, it is important to learn how to use it effectively in your C++ projects.
raw pointer from a smart pointer
In modern C++, the get()
and data()
methods are both used to obtain a raw pointer from a smart pointer. The main difference between the two methods is in the type of the returned pointer:
get()
: This method returns a raw pointer of the type specified by the smart pointer template parameter. For example, if you have a std::unique_ptr<int>
instance, calling get()
will return a raw int*
pointer. If you have a std::shared_ptr<char[]>
instance, calling get()
will return a raw char*
pointer.
#include <memory>
std::unique_ptr<int> ptr(new int(42));
int* raw_ptr = ptr.get();
data()
: This method is a member function of the std::vector
class and some other standard library containers, as well as some other C++ classes that provide a contiguous block of memory, such as std::unique_ptr
and std::shared_ptr
. This method returns a raw pointer to the beginning of the underlying memory block managed by the object. The type of the returned pointer is the same as the value type of the container or the type of the object being managed by the smart pointer.
#include <memory>
#include <vector>
std::vector<int> vec = { 1, 2, 3, 4, 5 };
int* raw_ptr = vec.data();
std::shared_ptr<char[]> buffer(new char[256]);
char* raw_buffer = buffer.data();
Note that data()
is only available for classes that manage contiguous memory, and is not available for all smart pointer types. Also note that data()
is only available since C++11.
Both get()
and data()
are useful for passing a raw pointer to C-style functions that expect a pointer to a raw buffer. However, using smart pointers instead of raw pointers is generally considered safer and less error-prone, as smart pointers automatically manage the memory allocation and deallocation, helping to prevent memory leaks and other issues.
Real world example
Often I use custom data structure to be filled and sent via basic UDP protocol to remote server:
{
unsigned char *out_buffer = (unsigned char*) malloc(sizeof(tmpHeartBeat));
memcpy(out_buffer, (const unsigned char*)&tmpHeartBeat, sizeof(tmpHeartBeat));
sendto(sockfd, out_buffer, sizeof(tmpHeartBeat), MSG_CONFIRM,
(const struct sockaddr *) &servaddr, sizeof(servaddr));
free(out_buffer);
}
Curly braces are used to limit the scope of the variable out_buffer
, so that it is destroyed after the sendto()
call. This is necessary because the memory allocated by malloc()
is not automatically freed when the function returns. The free()
function is used to free the memory allocated by malloc()
.
However, applying a modern C++ concept of smart pointers this part could be refactored as:
// Allocate a buffer using std::make_shared and initialize it with tmpHeartBeat
const auto out_buffer = std::make_shared<std::vector<unsigned char>>(
reinterpret_cast<const unsigned char*>(&tmpHeartBeat),
reinterpret_cast<const unsigned char*>(&tmpHeartBeat) + sizeof(tmpHeartBeat));
sendto(sockfd, out_buffer->data(), sizeof(tmpHeartBeat), MSG_CONFIRM,
(const struct sockaddr *) &servaddr, sizeof(servaddr));
I suppose that this code is more readable and easier to understand. Moreover, it is more safe because the memory allocated by std::make_shared
is automatically freed when the function returns. const
modifier is used as it helps to prevent accidental modification of variables and promotes code safety. Knowing that data structure is packed (declared with attribute __attribute__((packed)))
we can use a contigous memory block to allocate a buffer std::make_shared<std::vector> and initialize it with tmpHeartBeat.
You can then access the buffer using the data()
method of the std::vector
. For example:
unsigned char* raw_buffer = out_buffer.data();
As content of the structure tmpHeartBeat
is not going to be modified, it is possible to use reinterpret_cast
to convert tmpHeartBeat
to unsigned char*
type. This is a very powerful tool that allows you to convert a pointer of one type to a pointer of another type. However, it is important to use it carefully, as it can lead to undefined behavior if the types are not compatible. In this case, the types are compatible, as tmpHeartBeat
is a packed data structure, and unsigned char
is a primitive type.