A custom deleter is a function called by a smart pointer in place of the normal delete expression when the owned object is to be destroyed. Both std::unique_ptr and std::shared_ptr support custom deleters and are used to manage resources that are not cleaned up with delete; for example, a file handle or memory allocated with something other than the C++ allocator.
By default, a std::unique_ptr<T> will call delete on the pointer that it holds. If, instead, the resource is to be released in another way, a deleter can be provided as the second template argument. The most common example is a FILE*, which must be closed with fclose:
#include <iostream>
#include <memory>
#include <cstdio>
int main()
{
// Call this function with the provided arguments
std::unique_ptr<FILE, decltype(&fclose)> file{ fopen("test.txt", "w"), fclose };
if (file)
std::cout << "file opened successfully\n";
return 0;
}
The type of the unique pointer is std::unique_ptr<FILE, decltype(&fclose)>; the second type template parameter is the type of the deleter; in this case, a function pointer. The constructor takes the raw FILE* that fopen returns, and the function fclose. When the unique pointer is destroyed, it calls fclose on the stored pointer. If fopen fails, it returns a null pointer, which the unique pointer will treat safely.
The same technique works for any normal function that takes a single pointer. Suppose a resource is allocated using a C like function acquire and freed using release; then:
#include <iostream>
#include <memory>
int* acquire()
{
std::cout << "resource acquired\n";
return new int{ 42 };
}
void release(int* p)
{
std::cout << "resource released\n";
// Release the dynamically allocated object
delete p;
}
int main()
{
std::unique_ptr<int, decltype(&release)> p{ acquire(), release };
if (p)
std::cout << "owned value is " << *p << '\n';
return 0;
}
Here, the type of the unique pointer is std::unique_ptr<int, decltype(&release)> and it is called with the pointer returned by acquire() as the first argument and the address of the function release as the second. When p goes out of scope, it will call release on its stored pointer, which prints the message and deletes the integer. The programmer will never write delete by himself.
std::shared_ptr also supports custom deleters. A std::shared_ptr does not store the deleter as part of its type information, but rather stores it as part of its control block. Thus, instead of being passed as a template parameter, it is passed as an additional argument to the constructor:
#include <iostream>
#include <memory>
#include <cstdio>
int main()
{
// Initialize file with the value returned by fopen
std::shared_ptr<FILE> file{ fopen("test.txt", "w"), fclose };
if (file)
std::cout << "file opened successfully\n";
return 0;
}
When the last std::shared_ptr owning this FILE* is destroyed, the control block will call fclose on the stored pointer. However, the type of the shared pointer would be std::shared_ptr<FILE>, and two shared pointers with different deleters can be assigned to each other.
So, custom deleters allow smart pointers to manage resources not handled by a simple delete. For unique_ptr, deleters become part of the type, whereas they are only added to the control block of std::shared_ptr. In both instances, the smart pointer guarantees that the specified cleanup function is called exactly once, at the end of ownership.
Exercise: Custom DeletersStarter code, hints and solutionOpenClose
Practice: Custom Deleters
Work through the mission, reveal hints only when needed, and compare your result with the model solution.
Your challenge
Change the acquired int from 42 to 7 and predict the value and cleanup messages. Keep the custom release function as the unique_ptr deleter.
Mission checklist
- Run the starter as a baseline, then predict the change requested in Your challenge.
- Make that change in your editor; use the lesson-specific hints if you need help.
- Compile and run your answer, then compare with the worked solution and explain the result.
Starter program
This is the working baseline, not the completed answer. Compile as C++20, then make the change described above.
#include <iostream>
#include <memory>
int* acquire()
{
std::cout << "resource acquired\n";
return new int{ 42 };
}
void release(int* p)
{
std::cout << "resource released\n";
// Release the dynamically allocated object
delete p;
}
int main()
{
std::unique_ptr<int, decltype(&release)> p{ acquire(), release };
if (p)
std::cout << "owned value is " << *p << '\n';
return 0;
}Hints
The second unique_ptr template argument is the type of the release function pointer. The constructor supplies both the acquired pointer and that cleanup function.
At p's scope exit, its deleter calls release, which prints and deletes the int. Do not call release(p.get()) manually while p still owns the same allocation.
Worked solution
acquire now initializes the owned int to 7 rather than 42. The same custom release function prints its cleanup message and deletes the pointer when the unique_ptr leaves scope.
#include <iostream>
#include <memory>
int* acquire()
{
std::cout << "resource acquired\n";
return new int{ 7 };
}
void release(int* p)
{
std::cout << "resource released\n";
// Release the dynamically allocated object
delete p;
}
int main()
{
std::unique_ptr<int, decltype(&release)> p{ acquire(), release };
if (p)
std::cout << "owned value is " << *p << '\n';
return 0;
}
Example output
resource acquired
owned value is 7
resource released
0 comments
No comments yet. Be the first.