The PImpl idiom and the C++26 std:indirect type

mariusbancila.ro

59 points by signa11 6 hours ago


amluto - 5 hours ago

I have a little class called EImpl that is kind of like std::indirect except that it embeds the impl instead of pointing to it. It takes three template parameters: an embedded struct, a size and an alignment. It static_asserts that the embedded struct fits in the size and alignment, and it embeds it with approximately zero overhead. It’s about as easy to use as any other pImpl technique.

ryani - 2 hours ago

Sadly, you can't easily do the full pimpl idiom in C++.

The pimpl idiom is a C idiom where a header declares an opaque structure and prototypes of functions that take pointers to that structure. In C the OP example would look something like

    // widget.h
    typedef struct Widget_t Widget; /* opaque! */

    Widget* Widget_Create(const string* pName);
    Widget* Widget_Clone(Widget*);
    void Widget_Destroy(Widget*);

    void Widget_click(Widget*);
    int Widget_clickCount(const Widget*);
    const string* Widget_label(const Widget*);

    // widget.c
    struct Widget_t {
        int clicks;
        string *name;
    };
    // ... implementations of the functions from the .h ...
In particular, in C `Widget` directly has `clicks` and `name` as fields.

But in c++ we like to use methods on objects, and in order to do this, you need the class declaration in scope, which means your current compilation unit needs to have seen all of Widget's data members. In practice this means if you try to use "pimpl" in C++, you do something like the OP where there is a pointer to an opaque type inside your class.

However, this is not the same thing. Methods are called with a `this` pointer, which means every access to the internal structure adds a second pointer dereference. This is why this isn't the true pimpl -- it wastes an extra deref on every access.

You can get true pimpl in current C++ but it's a lot of boilerplate and heavily relies on compiler inlining. An implementation of the example from the OP: https://godbolt.org/z/6EznxeG1n . In practice this is too much work, hard to read, and so nobody does it.

For the c++ standards committee: please add an "opaque class" feature where the class can only define non-virtual method prototypes. Then the full class declaration, in the associated cpp file, could include its parent classes, actual data layout, and function implementations.

Panzerschrek - 5 hours ago

> Never null: it always holds a value, except in the moved-from state

I am wondering why C++ can't implement "non-null" unique_ptr version in the same way? As I know, that the main argument against implementing it is, that it's can't be done, since move-out unique_ptr still can be null.

zabzonk - 5 hours ago

Hmm. Do people use PIMPL that much (I have used it, but rarely) that we need std library support (and testing, documentation, understanding)? Just asking.

whizzter - 3 hours ago

Where are we with modules, isn't pimpl there largely to avoid costs related to including the world?

I was pondering on why he was putting the defaulted methods in the cpp, any particular reasons?

I did realize that the indirect version is required to be in the cpp since the header won't know how to copy without knowing the definition of the impl class.

3form - 5 hours ago

This looks great indeed - I wonder if there are any particular gotchas, though, as things often are in C++next land.

With many of the features coming into the language over time, I kinda wish that a bit more restricted subset of it eventually becomes a thing, but I know in practice it might as well be a completely different language. That, and I expect that still many other things have not been resolved as well as they are elsewhere, such as build system and dependency management (although I haven't touched this stack for a while now, so I would love to be surprised).

smallstepforman - 3 hours ago

Oh god, what monstrocity have we created?!?

All this complexity follows unique_ptr and copy constructor madness.

Anything with pointers with ownership should never be copied - period. Reference pointers - OK if scope/lifetime is known.

Can we have c++11 lite?

einpoklum - 4 hours ago

The example is problematic, in that:

1. click() should not be a member of the widget. A widget does not click; a user clicks a widget. A click can change a widget's state, but the state might change because of other effects, e.g. pressing a key when the widget is focused. But then, that's just one of the issues with treating UI widgets this way.

2. More to the point - clickCount. If this is a button, it shouldn't keep a record, or aggregate, of its clicks within it; and if it's a widget where this does really matter, like a range control where more clicks mean a value that goes farther along the range - you still would not keep the count of clicks, but the current position. Statistics about the interaction with an object should not be part of the object itself. At most it might be legitimate to have, say, a Widget class, a template like <class Stats> StatisticsTracker , and then class TrackedWidget which uses that as a mixin, i.e. inheriting both Widget and StatisticsTracker<ClickStats>. And that's already stretching it beyond what I would find reasonable.

3. Having something named is another aspect of objects which may be a good fit for a mixin class.

Anyway, an 'indirect' type for objects you don't know the definition of sounds nice.

A few more nitpickis about the example:

1. Instead of explicitly applying the rule-of-0 with `= default` for the copy&move ctor&assignment and the destructor - just _don't_ write anything:

    class Widget
    {
    public:
        void click();
        int  clickCount() const;
        std::string label() const;
    private:
        struct Impl;
        std::indirect<Impl> pimpl_;
    };
and that's the beauty of the rule of 0.

2. Why return an std::string for the label? The label() method should return an std::string_view

z0ltan - 2 hours ago

[dead]

Yomguithereal - 4 hours ago

[flagged]

shevy-java - 6 hours ago

C++ is getting more and more complex. It used to be said that people use only a small percentage of it when writing C++, but I am beginning to think that the cake is a lie here.

coffeeaddict1 - 6 hours ago

This is actually useful, but despite it is another extra thing you will have to remember when reading C++ code. I guess with LLMs things aren't so bad.

AnaSpelunker - 2 hours ago

I thought C++ is unnecessarily complex, and then I see Rust following the same pattern... I've just thought of a complexity metric that would calculate the ratio of alphanumeric characters to punctuation.