[deleted]
And before it grows too big, it wastes memory. For your use cases it may not matter, and the saved pointer indirection may be more important, but maybe the person who has a million item vector of objects doesn't appreciate a 300% "just in case" memory overhead. The overhead may also hurt cache hits.
If you're doing this to save the pointer indirection, you should benchmark it for every use case, since negative cache effects may dwarf that gain.
Then again, extra padding can also help performance, for some workloads (especially multi threaded read/write against a vector of objects).
So without further context, there's no way to say if your way hurts or helps. It's certainly not a general solution.
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.
https://github.com/microsoft/GSL/blob/main/docs/headers.md#u...
What do you mean by move-out unique_ptr? That the not_null ptr type would ne null after it's been moved?
In that case that's just a plain usage error, same as how you could memset it to null.
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.
I also like to use it sometimes to "hide" private methods and their documentation into PIMPL, so the public header is kept clean.
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).
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.
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
[dead]
[dead]
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?
People will contend themselves with "C++ the good parts", helped by clang-tidy, PVS, MSVC analyse, and move on.
So you're only trying to solve compile time issues (incremental and not)? Maybe the right long term solution is C++ modules, instead? And maybe "just" a matter of having your build environment support modules?
GNURadio consistently uses pimpl for blocks, as I understand it mainly for ABI.
> willing to stop improving.
I think that dismissing it like that shows a naive understanding of execution environments, binary interface design, and in general systems software engineering.
I would love to proved wrong, but everything I can think of still leaves a footgun that's easy to trigger by accident, and thus negates the point of the solution.
I think the can't-reference-after-moved-from and objects-are-not-Copy-by-default are key to creating these types (at least enforced at compile time). And that would require major language changes, at least as big as the C++11 changes.
While many of us that like C++, would wish for a different evolution process, some of this stuff can be enforced by static analysis tooling.
Just like despite being safer than C++, we still use Sonar, FindBugs, FxCop/Roslyn Analysers, go vet, rust clippy, one more reason to actually use Sonar, clang-tidy, PVS, MSVC analyse,... with languages like C and C++.
In some of these you can add your own rules even, even if not always that straightforward.
Yep, that's what I've used it for. Didn't find it too difficult to implement it myself, but I guess every bit of convenience/bug avoidance helps.
Not sure how much pimpl is used in reality, but it's a pretty ok solution to speed up build times (apart from unity builds), because it avoids having to include headers that are only needed for the private state into the public interface header.
#define WIN32_LEAN_AND_MEAN 1
#include <windows.h>
In precompiled headers to solve that particular problem.To make hobby-coding fun, i use a mstdp.hpp that implements "naive" versions of unique,shared,function,etc that compiles faster than including just one of the std versions (and yes, MSVC versions of those libraries seem to be excessivly complex).
You'd essentially like to derive a class/module implementation from it's corresponding class/module interface (which is all the user sees), but have the language automatically add a hidden "pimpl" pointer to the interface class. The implementation would then essentially use "this" to access public members, and "pimpl" for private members.
> 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:
The blog post explicitly explains why this doesn't work. You have to define these methods in the source file because they need to see the definition of the Impl struct.
This only works if it's always the same value. This doesn't work if the label is for example, set to `std::to_string(clickCount())`
[deleted]
This "std::indirect" tries to have value semantics, but in fact it's just another type of smart pointer and uses pointer syntax (pimpl->foo : forced since C++ allows "->" as a user defined operator name, but not ".").
But it's a weird sort of "pointer" given this copying behavior, which is maybe why they didn't give it a "_ptr" name.
For me your comment seems pointless, because std::indirect have a precise and clear problem which it solves. And this is totaly not a "hackery fix of language drawback". In this case language works as intended and std::indirect just close the gap for facility which is still would be written by hands, if there is no std::indirect.
> In large codebases you will now have to deal with all three solutions being used, depending on how old the code is.
this is really a problem of "large codebases" and not C++ complexity. If people writing badly organized code.. than.. nothing will help them. Even such string language as Rust.
---
But, I go aside. I can be wrong here, but this is how I see this: - author author worked with C++ early in his career - now his work with something like python or javascript or "promptscript" or don't programming at all - he managed to taste C++ drawbacks - now he reading such article just to see "what's new" in technology he was interested in the past. - he see something what he don't understand from first sight - this looks like a complex and quirky thing => he decide that problem in the language, not in his incompetence in this area
I see such comments very often and usually people don't provide any technical expertise. But just their feeling that "oh, this thing was so complex, now it even more complex".
When you write new code, this is (mostly) not an issue; when you have to maintain old code, it is. Especially if the existing codebase is somewhat of a patchwork of code introduced at different points in time - pre-C++98, C++98, C++03, C++11 and so on. For this reason it is a saintly virtue to manage to unify the C++ "vernacular" used in a project, for better readability by newcomers and for facilitating uniform changes to the entire codebase later on.
> Still best only used in implementation files, not headers.
We only compile implementation files!
Because they spend so much of their time struggling with the pain points of the older code.
> but can't intuitively understand what it's doing
For (most?) new vocabulary types, it is rather intuitive to understand what they do. optional, variant, indirect - you may not remember the details by heart immediately, but you get the general idea and expect that they would behave in some reasonable way. And mostly, they do. That's not to say they're perfect: I feel like vomiting looking at std::variant's and how you have to work with them, as opposed to a proper case classes / algebraic union types in the language itself. And yet - when someone puts one in their class, instead of a bunch of code in a bunch of methods, you know what's going on. It does "read like a different language" somewhat, and that's good. The nicer language has been struggling to get out, as the saying goes.
Even classes are an instance of this, they were to solve some perceived problems, but they created much bigger issues, such as readability issues and introducing many more compile time dependencies.
PIMPL wasn't even a C++ feature but an idiom pushed by some people. It is next to unusable because you have to duplicate the API and write all the call forwards.
One problem with std::unique_ptr for example is that there is no ergonomic way to use it to hide implementations. The reason is it relies on destructors and to use destructors the class definition needs to be visible.
This here should not need to happen:
> document that moved-from objects cannot be used
It's moved from, so of course it cannot be used. Shouldn't have to add an assertion in every method to guard a fundamental invariant.
Nobody needs "deep copying", ever. It's not even well defined what it should mean (i.e. how deep etc.). It's purely a theoretical problem with no good practical (one-fits-all) solution. The only practical way is to copy what you need copied, when you need it. Done.
This way I have already replaced a lot of for-loops by range-based for-loops. It helps me to understand code faster.
But code parts that noone needs to touch or see do not need to be more readable.
C++ is trivial compared to the code I work on. If you are writing hello world complexity then C++ might be complex but some of us work on hard problems.
I would argue that if cleaning up resources properly is among the hard problems, or among the most error-prone problems in your code, then maybe you're problems aren't that hard or complex after all.
I'm currently working on a distributed caching system and on real time voxel geometry boolean operations simulation (on GPU), both on the scale of >= 10^9. Is that "complex" enough? Both are done in C++. C++ helps exactly 0 in achieving any of these things (as opposed to using plain C), well the one help is I don't have to type 'struct' all the time.
In fact, in one of these projects I was pushed to use STL initially. I'm now working on getting rid of the last of them because we have had concurrency bugs and performance problems from using them. The code was not obvious and using STL containers (std::deque is very bad specifically) meant the actual runtime characteristics depend on which STL implementation is being compiled in. It would have been easier to just do straightforward obvious manual code.
> (as opposed to using plain C)
Also, curious - how plain C helps with this?