Custom deleters
A unique_ptr has a second template parameter, the deleter: the function or object it calls instead of delete. That is how the same type owns a FILE, a socket, a handle from a C library or an array.
The deleter is part of the pointer's type, so it costs nothing when it is empty and it is chosen once, at the declaration. This lesson wraps a FILE three ways and ends with the declaration that forgets to pass the deleter.
A deleter also runs when the pointer is reset or replaced, which is how a class can own a handle that changes: assign a new pointer and the old handle is closed before the new one is stored.
A FILE closed by fclose
std::unique_ptr<std::FILE, decltype(&std::fclose)> stores a pointer to FILE and a function pointer to fclose. The constructor takes the result of fopen and the function, and the destructor calls fclose.
The program opens a file, writes to it through get() and lets the pointer go out of scope. The cat command shows the text, which is the proof that fclose ran and flushed the buffer.
get() returns the raw pointer without giving up ownership, which is what a C function needs. release() would give up ownership and require a manual close, and reset() would close the current file and take a new one.
#include <cstdio>
#include <iostream>
#include <memory>
int main()
{
std::unique_ptr<std::FILE, decltype(&std::fclose)> file{std::fopen("note.txt", "w"), &std::fclose};
if (file) {
std::fputs("data written through a unique_ptr", file.get());
std::cout << "written\n";
}
}
g++ -std=c++23 -Wall -Wextra file.cpp -o file_owner
./file_owner
cat note.txt
written
data written through a unique_ptr
A deleter as a function object
A function object is a deleter with state. The closer below counts what it closed, and the pointer stores one by value, so the count survives until the pointer is destroyed.
The program opens a file, prints whether it opened and lets the closer run at the end of main. The message comes from the deleter, which is the one place the close happens.
An empty function object costs no space thanks to the empty base optimisation, which is why the default delete does not grow the pointer. A stateful one adds its size to the pointer.
#include <cstdio>
#include <iostream>
#include <memory>
struct closer
{
void operator()(std::FILE* file) const
{
if (file != nullptr) {
std::fclose(file);
std::cout << "closed by the deleter\n";
}
}
};
int main()
{
std::unique_ptr<std::FILE, closer> file{std::fopen("log.txt", "w")};
std::cout << (file != nullptr) << '\n';
}
g++ -std=c++23 -Wall -Wextra functor.cpp -o functor
./functor
1
closed by the deleter
A lambda as the deleter
A lambda is a function object with a type the compiler writes, so the pointer's type is written with decltype of the lambda variable. The lambda runs when the pointer is destroyed and can print what it releases.
The program allocates one int, stores it with a lambda that prints and deletes, and lets it go out of scope. The output is the message from the deleter.
The lambda form is convenient for a one-off resource and awkward for a type that appears in an interface, because the type of the lambda cannot be spelled by a caller. A named function object is the better choice there.
#include <iostream>
#include <memory>
int main()
{
auto report = [](int* value) {
std::cout << "deleting " << *value << '\n';
delete value;
};
std::unique_ptr<int, decltype(report)> value{new int{7}, report};
}
g++ -std=c++23 -Wall -Wextra lambda.cpp -o lambda_deleter
./lambda_deleter
deleting 7
An array needs delete[]
std::make_unique<int[]>(3) allocates an array and uses delete[] as its deleter. The pointer supports operator[] and does not support *, because an array is not one object.
The program fills three elements and prints their sum. The bracket on the pointer is the same subscript as on a raw array, and the size is the one passed to make_unique.
The array form comes from C++14 and is the reason a unique_ptr can replace new int[n] without a custom deleter. A std::vector is still the better choice when the size changes.
#include <iostream>
#include <memory>
int main()
{
auto values = std::make_unique<int[]>(3);
values[0] = 1;
values[1] = 2;
values[2] = 3;
int sum = 0;
for (int i = 0; i < 3; ++i) {
sum += values[i];
}
std::cout << sum << '\n';
}
g++ -std=c++23 -Wall -Wextra array.cpp -o array_owner
./array_owner
6
The deleter that is not passed
A function pointer has no default value, so a unique_ptr whose deleter is a function pointer cannot be constructed from the raw pointer alone. The declaration must pass the function as well.
The program passes only the FILE and the compiler reports that no constructor matches. The message lists the constructors, and none of them can build a deleter by itself.
The fix is the second argument, &std::fclose, as in the first section. A deleter type with a default constructor, such as the function object, does not need it.
#include <cstdio>
#include <memory>
int main()
{
std::unique_ptr<std::FILE, decltype(&std::fclose)> file{std::fopen("x.txt", "w")};
}
g++ -std=c++23 -Wall -Wextra no_deleter.cpp -o no_deleter
no_deleter.cpp: In function 'int main()':
no_deleter.cpp:6:85: error: no matching function for call to 'std::unique_ptr<_IO_FILE, int (*)(_IO_FILE*)>::unique_ptr(<brace-enclosed initializer list>)'
6 | std::unique_ptr<std::FILE, decltype(&std::fclose)> file{std::fopen("x.txt", "w")};
| ^
no_deleter.cpp:6:85: note: there are 9 candidates
In file included from /usr/include/c++/15.2.0/memory:80,
from no_deleter.cpp:2:
/usr/include/c++/15.2.0/bits/unique_ptr.h:385:9: note: candidate 1: 'template<class _Up, class> std::unique_ptr<_Tp, _Dp>::unique_ptr(std::auto_ptr<_Up>&&) [with <template-parameter-2-2> = _Up; _Tp = _IO_FILE; _Dp = int (*)(_IO_FILE*)]'
385 | unique_ptr(auto_ptr<_Up>&& __u) noexcept;
| ^~~~~~~~~~
/usr/include/c++/15.2.0/bits/unique_ptr.h:385:9: note: template argument deduction/substitution failed:
no_deleter.cpp:6:85: note: mismatched types 'std::auto_ptr<_Up>' and 'FILE*'
6 | std::unique_ptr<std::FILE, decltype(&std::fclose)> file{std::fopen("x.txt", "w")};
| ^
/usr/include/c++/15.2.0/bits/unique_ptr.h:374:9: note: candidate 2: 'template<class _Up, class _Ep, class> constexpr std::unique_ptr<_Tp, _Dp>::unique_ptr(std::unique_ptr<_Up, _Ep>&&) [with _Ep = _Up; <template-parameter-2-3> = _Ep; _Tp = _IO_FILE; _Dp = int (*)(_IO_FILE*)]'
374 | unique_ptr(unique_ptr<_Up, _Ep>&& __u) noexcept
| ^~~~~~~~~~
/usr/include/c++/15.2.0/bits/unique_ptr.h:374:9: note: template argument deduction/substitution failed:
no_deleter.cpp:6:85: note: mismatched types 'std::unique_ptr<_Tp, _Dp>' and 'FILE*'
6 | std::unique_ptr<std::FILE, decltype(&std::fclose)> file{std::fopen("x.txt", "w")};
| ^
/usr/include/c++/15.2.0/bits/unique_ptr.h:353:19: note: candidate 3: 'template<class _Del, class> constexpr std::unique_ptr<_Tp, _Dp>::unique_ptr(std::nullptr_t) [with <template-parameter-2-2> = _Del; _Tp = _IO_FILE; _Dp = int (*)(_IO_FILE*)]'
353 | constexpr unique_ptr(nullptr_t) noexcept
| ^~~~~~~~~~
/usr/include/c++/15.2.0/bits/unique_ptr.h:353:19: note: template argument deduction/substitution failed:
/usr/include/c++/15.2.0/bits/unique_ptr.h: In substitution of 'template<class _Tp, class _Dp> template<class _Up> using std::unique_ptr<_Tp, _Dp>::_DeleterConstraint = typename std::__uniq_ptr_impl<_Tp, _Up>::_DeleterConstraint::type [with _Up = int (*)(_IO_FILE*); _Tp = _IO_FILE; _Dp = int (*)(_IO_FILE*)]':
/usr/include/c++/15.2.0/bits/unique_ptr.h:352:37: required from here
352 | template<typename _Del = _Dp, typename = _DeleterConstraint<_Del>>
| ^~~~~~~~
/usr/include/c++/15.2.0/bits/unique_ptr.h:274:15: error: no type named 'type' in 'using std::__uniq_ptr_impl<_IO_FILE, int (*)(_IO_FILE*)>::_DeleterConstraint = struct std::enable_if<false, void>' {aka 'struct std::enable_if<false, void>'}
274 | using _DeleterConstraint =
| ^~~~~~~~~~~~~~~~~~
/usr/include/c++/15.2.0/bits/unique_ptr.h:347:9: note: candidate 4: 'template<class _Del, class _DelUnref> constexpr std::unique_ptr<_Tp, _Dp>::unique_ptr(pointer, std::__enable_if_t<((bool)std::is_lvalue_reference<_Del>::value), _DelUnref&&>) [with _DelUnref = _Del; _Tp = _IO_FILE; _Dp = int (*)(_IO_FILE*)]' (deleted)
347 | unique_ptr(pointer,
| ^~~~~~~~~~
/usr/include/c++/15.2.0/bits/unique_ptr.h:347:9: note: candidate expects 2 arguments, 1 provided
/usr/include/c++/15.2.0/bits/unique_ptr.h:338:9: note: candidate 5: 'template<class _Del, class> constexpr std::unique_ptr<_Tp, _Dp>::unique_ptr(pointer, std::__enable_if_t<((bool)(! std::is_lvalue_reference<_Del>::value)), _Del&&>) [with <template-parameter-2-2> = _Del; _Tp = _IO_FILE; _Dp = int (*)(_IO_FILE*)]'
338 | unique_ptr(pointer __p,
| ^~~~~~~~~~
/usr/include/c++/15.2.0/bits/unique_ptr.h:338:9: note: candidate expects 2 arguments, 1 provided
/usr/include/c++/15.2.0/bits/unique_ptr.h:325:9: note: candidate 6: 'template<class _Del, class> constexpr std::unique_ptr<_Tp, _Dp>::unique_ptr(pointer, const deleter_type&) [with <template-parameter-2-2> = _Del; _Tp = _IO_FILE; _Dp = int (*)(_IO_FILE*)]'
325 | unique_ptr(pointer __p, const deleter_type& __d) noexcept
| ^~~~~~~~~~
/usr/include/c++/15.2.0/bits/unique_ptr.h:325:9: note: candidate expects 2 arguments, 1 provided
/usr/include/c++/15.2.0/bits/unique_ptr.h:311:9: note: candidate 7: 'template<class _Del, class> constexpr std::unique_ptr<_Tp, _Dp>::unique_ptr(pointer) [with <template-parameter-2-2> = _Del; _Tp = _IO_FILE; _Dp = int (*)(_IO_FILE*)]'
311 | unique_ptr(pointer __p) noexcept
| ^~~~~~~~~~
/usr/include/c++/15.2.0/bits/unique_ptr.h:311:9: note: template argument deduction/substitution failed:
/usr/include/c++/15.2.0/bits/unique_ptr.h:298:19: note: candidate 8: 'template<class _Del, class> constexpr std::unique_ptr<_Tp, _Dp>::unique_ptr() [with <template-parameter-2-2> = _Del; _Tp = _IO_FILE; _Dp = int (*)(_IO_FILE*)]'
298 | constexpr unique_ptr() noexcept
| ^~~~~~~~~~
/usr/include/c++/15.2.0/bits/unique_ptr.h:298:19: note: candidate expects 0 arguments, 1 provided
/usr/include/c++/15.2.0/bits/unique_ptr.h:360:7: note: candidate 9: 'constexpr std::unique_ptr<_Tp, _Dp>::unique_ptr(std::unique_ptr<_Tp, _Dp>&&) [with _Tp = _IO_FILE; _Dp = int (*)(_IO_FILE*)]'
360 | unique_ptr(unique_ptr&&) = default;
| ^~~~~~~~~~
/usr/include/c++/15.2.0/bits/unique_ptr.h:360:18: note: no known conversion for argument 1 from 'FILE*' to 'std::unique_ptr<_IO_FILE, int (*)(_IO_FILE*)>&&'
360 | unique_ptr(unique_ptr&&) = default;
| ^~~~~~~~~~~~
Exercises
- Wrap a std::FILE for reading and read one line with fgets through get().
- Give the closer a counter member and print it after the pointer is destroyed.
- Replace the array program with a std::vector and check that no size has to be passed to the allocation.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What is the second template parameter of a unique_ptr?
Answer
The deleter type
The deleter says what to call instead of delete when the pointer is destroyed.
-
Why does the FILE pointer need a custom deleter?
Answer
Because a FILE must be closed with fclose, not deleted
The C library owns the FILE and expects fclose; delete on it is undefined behaviour.
-
Which type owns a FILE opened with fopen and closed with fclose?
Answer
std::unique_ptr<std::FILE, decltype(&std::fclose)>The deleter type is part of the pointer, and a function pointer has no default, so it is written in the type and passed at construction.
-
What does
get()do on a unique_ptr?Answer
Returns the raw pointer without giving up ownership
get() is for passing the handle to a C function that does not take ownership.
-
What does this program print?
auto d = [](int* p) { std::cout << *p; delete p; }; std::unique_ptr<int, decltype(d)> v{new int{7}, d};Answer
7
The lambda runs at the end of main and prints the value before deleting it.
-
How is an array allocated for
std::make_unique<int[]>(3)freed?Answer
With delete[]
The array form of unique_ptr uses delete[] as its deleter.
-
What does
std::make_unique<int[]>(3)return?Answer
A pointer to three value-initialised ints
The size in brackets is the array length, and the elements are value-initialised to zero.
-
What does the compiler do with
std::unique_ptr<std::FILE, decltype(&std::fclose)> f{std::fopen("x", "w")};?Answer
It refuses: the deleter has no default and was not passed
A function pointer deleter has no default, so the constructor needs it as the second argument.
-
What is the size of a unique_ptr with an empty (stateless) deleter on this build?
Answer
One pointer
An empty deleter is optimised away, so the pointer stores only the address.
-
This program prints
1and thenclosed by the deleter. When does the closer run?#include <cstdio> #include <iostream> #include <memory> struct closer { void operator()(std::FILE* file) const { if (file != nullptr) { std::fclose(file); std::cout << "closed by the deleter\n"; } } }; int main() { std::unique_ptr<std::FILE, closer> file{std::fopen("log.txt", "w")}; std::cout << (file != nullptr) << '\n'; }Answer
At the end of main, when the unique_ptr is destroyed
The deleter runs in the destructor of the pointer, which is at the end of main here.
The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.
Article author: Arthur Isaev