The PImpl idiom and the C++26 std::indirect type
July 23, 2026
by
Marius Bancila
PImpl (which stands for Pointer to implementation) is a programming tehnicque to remove implementation details from a class by placing them in a separate class that is accessed through an opaque pointer. Its purpose is to separate interfaces and implementations and minimize compile-time dependencies. In this article, we’ll at how the PImpl idiom is typically implemented in C++ and how C++26 simplifies its implementation.
Implementing with a raw pointer
We can implement the Pimpl idiom using raw pointers and abiding to the Rule of Five. This is a guideline that says that when a class defines a special member function (for resource management) it should define all five of them (destructor, copy constructor, copy assignment operator, move constructor, move assignment operator).
To demonstrate this, we will use a widget that represents a UI element that has a label and can be clicked and on each click a counter is incremented. Therefore, a widget would have a counter and a label but these are implementation details that are hidden behind an implementation class.
This
Widget
class definition looks as follows:
#pragma once
#include
class Widget
{
public:
Widget(const std::string& name);
~Widget();
Widget(const Widget& other);
Widget& operator=(const Widget& other);
Widget(Widget&& other) noexcept;
Widget& operator=(Widget&& other) noexcept;
void click();
int clickCount() const;
std::string label() const;
private:
struct Impl;
Impl* pimpl_;
};
The
Widget::Impl
class is an incomplete type here. It’s forward declared and defined in the .cpp file. The
Widget
class keeps a pointer to an object of this type.
#include “Widget.h”
struct Widget::Impl
{
std::string name;
int clicks = 0;
explicit Impl(std::string n) : name(std::move(n)) {}
};
Widget::Widget(const std::string& name)
: pimpl_(new Impl(name))
{
}
Widget::~Widget()
{
delete pimpl_;
}
Widget::Widget(const Widget& other)
: pimpl_(new Impl(*other.pimpl_))
{
}
Widget& Widget::operator=(const Widget& other)
{
if (this != &other)
{
Impl* tmp = new Impl(*other.pimpl_);
delete pimpl_;
pimpl_ = tmp;
}
return *this;
}
Widget::Widget(Widget&& other) noexcept
: pimpl_(other.pimpl_)
{
other.pimpl_ = nullptr;
}
Widget& Widget::operator=(Widget&& other) noexcept
{
if (this != &other)
{
delete pimpl_;
pimpl_ = other.pimpl_;
other.pimpl_ = nullptr;
}
return *this;
}
void Widget::click() { ++pimpl_->clicks; }
int Widget::clickCount() const { return pimpl_->clicks; }
std::string Widget::label() const { return pimpl_->name; }
The
Widget
class defines all five special member functions. The move constructor and assignment operator are defined
noexcept
because they just copy/delete objects and nothing should throw. Moreover, it’s a performance issue because a container such as
std::vector
will only move elements during reallocation if the move constructor is
noexcept
; otherwise it falls back to copying to preserve its strong exception guarantee.
The
Impl
object is created during the construction of the
Widget
and basically stores the state of the widget. The public interface methods of
Widget
use it to access the state (clicks, name).
A
Widget
can be used as follows:
#include
#include
#include “Widget.h”
int main()
{
Widget a(“Button A”);
a.click();
a.click();
std::println(“clicks {}”, a.clickCount()); // prints “clicks 2”
Widget b = a;
b.click();
std::println(“clicks {}”, b.clickCount()); // prints “clicks 3”
}
A problem that may seem subtle is that constness is not propagated from
Widget
to
Impl
. In a
const
method, such as
clickCount()
above, we can change the state because even if the
Impl
pointer is const, the object it points to is not. Therefore, the following would compile and produce unexpected results:
int Widget::clickCount() const { return ++pimpl_->clicks; }
On the other hand, a consequence of the move semantics is that a moved-from
Widget
object has a null
pimpl_
pointer and calling any function that use the pointer is undefined behavior (UB).
Widget a(“Button A”);
Widget b = std::move(a);
a.click(); // undefined behavior
There are several ways to address this:
document that moved-from objects cannot be used
add a check that
pimpl_
is not null everywhere before its usage
provide a function that indicates whether the object is in a valid state so clients can query whether they can use the widget or not
Implementing using std::unique_ptr
We can simplify the implementation by using the C++11
std::unique_ptr
type instead of a raw pointer. This automatically manages the allocated object so we don’t have to do resource management explicitly.
#pragma once
#include
#include
class Widget
{
public:
Widget(const std::string& name);
~Widget();
Widget(Widget&&) noexcept;
Widget& operator=(Widget&&) noexcept;
Widget(const Widget& other);
Widget& operator=(const Widget& other);
void click();
int clickCount() const;
std::string label() const;
private:
struct Impl;
std::unique_ptr pimpl_;
};
#include “Widget.h”
struct Widget::Impl
{
std::string name;
int clicks = 0;
explicit Impl(std::string n) : name(std::move(n)) {}
};
Widget::Widget(const std::string& name)
: pimpl_(std::make_unique(name))
{
}
Widget::~Widget() = default;
Widget::Widget(Widget&&) noexcept = default;
Widget& Widget::operator=(Widget&&) noexcept = default;
Widget::Widget(const Widget& other)
: pimpl_(std::make_unique(*other.pimpl_))
{
}
Widget& Widget::operator=(const Widget& other)
{
if (this != &other)
{
*pimpl_ = *other.pimpl_;
}
return *this;
}
void Widget::click() { ++pimpl_->clicks; }
int Widget::clickCount() const { return pimpl_->clicks; }
std::string Widget::label() const { return pimpl_->name; }
In this implementation, the copy constructor and copy assignment are explicitly user-defined. The destructor, move constructor, and move assignment operator are defaulted to the compiler but this must happen in the .cpp file because in the header
Widget::Impl
is an incomplete type and the
unique_ptr
‘s deleter needs to know the size of Impl.
Although there is no manual handling of resources anymore, the constness issue remains the same as well as the null
pimpl_
object after a move.
Implementing using std::indirect
This is where the
std::indirect
enters the scene. This is a new vocabulary type from C++26, defined in the
header. It is intended to be used for class members that are dynamically allocated but need to behave like values. It’s designed to be used instead of
std::unique_ptr
where its semantics (not copyable, does not propagate const, and can be
null
) are not appropriate. The perfect example for this is Pimpl.
The following snippet shows the
Widget
class implemented using
std::indirect
:
#pragma once
#include
#include
class Widget
{
public:
Widget(const std::string& name);
Widget(const Widget&);
Widget(Widget&&) noexcept;
Widget& operator=(const Widget&);
Widget& operator=(Widget&&) noexcept;
~Widget();
void click();
int clickCount() const;
std::string label() const;
private:
struct Impl;
std::indirect pimpl_;
};
#include “widget.h”
struct Widget::Impl
{
std::string name;
int clicks = 0;
explicit Impl(std::string n) : name(std::move(n)) {}
};
Widget::Widget(const std::string& name)
: pimpl_(std::in_place, name)
{}
Widget::Widget(const Widget&) = default;
Widget::Widget(Widget&&) noexcept = default;
Widget& Widget::operator=(const Widget&) = default;
Widget& Widget::operator=(Widget&&) noexcept = default;
Widget::~Widget() = default;
void Widget::click() { ++pimpl_->clicks; }
int Widget::clickCount() const { return pimpl_->clicks; }
std::string Widget::label() const { return pimpl_->name; }
If we compare these implementation, we’ll see that the five special member functions are still declared in the header but they are defaulted (for compiler implementation) in the source file, where the
Impl
type is complete and known to the compiler (this is the same requirement as for
std::unique_ptr
).
The
std::indirect
type owns a single
T
object allocated on the heap, but behaves like a
T
member:
Deep copy
: copying the
std::indirect
object copies the owned object, so your class’s compiler-generated copy constructor just works.
Const propagation
: through a
const std::indirect
object, you only get const access to the
T
.
Never null
: it always holds a value, except in the moved-from state (but features a member function called
valueless_after_move()
that lets you check the state).
Using
std::indirect
does not prevent us from having to explicitly declare and default the special member functions but does help us with propagating constness, which means the code is more robust towards accidental changes.
The
valueless_after_move()
is basically a replacement for null check from
unique_ptr
. For instance, we can add an assert to all the functions that use the
pimpl_
object to ensure they are not invoked after the widget has been moved.
void Widget::click()
{
assert(!pimpl_.valueless_after_move() && “use of moved-from Widget”);
++pimpl_->clicks;
}
Another example of using
valueless_after_move()
is erase-remove from a vector from which we moved out objects. Here is an example:
std::vector<std::indirect> widgets = …;
std::vector<std::indirect> selected;
for (auto& w : widgets)
if (select(w))
selected.push_back(std::move(w)); // leaves a valueless widget behind
std::erase_if(widgets, [](const auto& w) {
return w.valueless_after_move(); // remove the dangling object
});
This is possible with
std::unique_ptr
too, but the check would be a
w == nullptr
. Therefore, it’s just a more verbose way of checking that an
std::indirect
object still holds a value or not.
std::indirect
comes with a companion,
std::polymorphic
, but we will look at that in another article.
At the time of writing this, only GCC 16 supports
std::indirect
.
See also
PImpl
Pimpl For Compile-Time Encapsulation (Modern C++)
Modern C++: The Pimpl Idiom
P3019R14: indirect and polymorphic: Vocabulary Types for Composite Class Design
Share this:
Click to share on Facebook (Opens in new window)
Facebook
Click to share on X (Opens in new window)
X
Click to print (Opens in new window)
Print
More
Click to email a link to a friend (Opens in new window)
Email
Click to share on Reddit (Opens in new window)
Reddit
Like this:
Like
Loading…
Categories
C++
Tags
C++
,
C++26
,
pimpl
Improvements to std::format in C++26
Leave a Reply
Cancel reply
This site uses Akismet to reduce spam.
Learn how your comment data is processed.
About me
Find me on:
Archives
July 2026
M
T
W
T
F
S
S
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
« Jun
Follow me on Twitter
My Tweets
Recent Posts
The PImpl idiom and the C++26 std::indirect type
Improvements to std::format in C++26
Multiplying matrices in C++
Universally Unique Lexicographically Sortable Identifiers (ULIDs)
Time-based Universally Unique Identifiers (UUIDs v7)
What is the C++ nth_element algorithm?
What’s new in C++26: contracts (part 3)
What’s new in C++26 (part 2)
What’s new in C++26 (part 1)
Erroneous behaviour has entered the chat
Meta
Log in
Entries feed
Comments feed
WordPress.org