C++ pre-allocated vector of objects containing pointer - c++

I have the following structure:
struct CacheNode {
set<int> *value;
int timestamp;
CacheNode() : value(new set<int>()), timestamp(0) {}
};
And I pre-allocate a vector of them as follows:
vector<CacheNode> V(10);
When I do this, every CacheNode element in the vector points to the same set<int> in its value field. In particular,
V[0].value->insert(0);
cout << V[1].value->size() << endl;
prints out 1 instead of the 0 that I want.
What is the correct way to pre-allocate the vector (or to declare the structure) so that each CacheNode have its own set<int> instance?
(Note: I do need the value to be a pointer to a set, because it is possible in my application for some CacheNodes to share sets.)

You have violated the rule of 3. You have created an object with a non-trivial constructor, and failed to create a destructor or copy constructor or operator=.
std::vector<blah> foo(10) creates a single default constructed blah, and makes 10 copies of it in foo. Because you violated the rule of 3, these 10 copies are all identical.
The easiest method would be to do away with the new:
struct CacheNode {
std::set<int> value;
int timestamp;
CacheNode() : value(), timestamp(0) {}
};
another route would be to use a unique_ptr for lifetime management, and explicitly copy:
struct CacheNode {
std::unique_ptr<std::set<int>> value;
int timestamp;
CacheNode() : value(new std::set<int>()), timestamp(0) {}
CacheNode(CacheNode&&) = default; // C++11 feature
CacheNode(CacheNode const& other):value(new std::set<int>( *other.value ) ), timestampe(other.timestamp) {}
CacheNode& operator=(CacheNode const& other) {
value.reset(new std::set<int>(*other.value));
timestampe = other.timestamp;
return *this;
}
CacheNode& operator=(CacheNode&& other) = default;
// no need for ~CacheNode, unique_ptr handles it
};
when you want to take the std::set<int> out of your CacheNode, call CacheNode().value.release() and store the resulting std::set<int>*.
std::shared_ptr<std::set<int>> would allow shared ownership of the std::set.
There are other approaches, including making your vector store pointers to CacheNode, creating value_ptr<T> templates that do value semantics, etc.
In C++11, these are relatively easy and safe, because std::vector will move things around, and move semantics on a value_ptr<T> won't create a new T.
I am a bit leery of your plan to have shared std::set<int> between different CacheNode, because in general that is bad smell -- the ownership/lifetime of things should be clear, and in this case you have some CacheNode that own the std::set<int> and others that don't (because they share ownership). A shared_ptr can get around this, but often there are better solutions.

vector<CacheNode> V(10); creates an initial CacheNode object and then copies it 10 times. So you have 10 identical objects.
You can use generate_n:
std::vector<CacheNode> v;
std::generate_n(std::back_inserter(v), 10u, [](){ return CacheNode{}; });
Here's an example program.

You will want to use vector.assign(10, CacheNode()) as what your doing is a way of reserving space mostly.
Also you should do what the other ones say, provide virtual destructor and such.

Related

Passing stack allocated value by reference to a constructor in C++

I'm working with an external library and there's some code that gives me pause. Basically, there's a vector that is allocated inside a loop (on the stack). That vector is then passed by reference to the constructor of some object, and used to initialize one of the object's vector fields, which was not declared as a reference. Is the newly created object holding a reference to something that no longer exists? Or is this just a more efficient way of copying the vector, in which case the fact that it was allocated on the stack makes no difference?
Here's a minimal example:
class Holder {
public:
Holder(vector<int>& vref) : vec(vref) {}
vector<int> vec;
}
Holder* MakeHolder() {
vector<int> v {1, 2};
return new Holder(v);
}
int main() {
Holder *h = MakeHolder();
}
There's no reference held to a departed object, but I certainly wouldn't call it "efficient". Without an std::move in that ctor-initialiser, the vector must be copied.
You could put std::move in there but then Holder would be a little confusing to use.
Personally I'd take the vector in by value, so the calling scope can std::move into it (or pass a temporary which will do this automatically), then std::move the constructor argument into the new member. That way you literally just have one vector the entire time.
class Holder {
public:
Holder(vector<int> vref) : vec(std::move(vref)) {}
vector<int> vec;
}
Holder* MakeHolder() {
vector<int> v {1, 2};
return new Holder(std::move(v)); // Or just `return new Holder({1,2});`
}
int main() {
Holder *h = MakeHolder();
}
And, this way, if you want to keep the original vector alive (not moved-from) then that's fine too! Just pass it in and it'll get copied. Things will "just work" without really needing to know what's inside the constructor code (you only need to know that it takes a value).
The other thing I'd change is introducing std::unique_ptr, because you currently have a memory leak:
class Holder {
public:
Holder(vector<int> vref) : vec(std::move(vref)) {}
vector<int> vec;
}
std::unique_ptr<Holder> MakeHolder() {
return std::make_unique<Holder>({1,2});
}
int main() {
auto h = MakeHolder();
}
(Some people would spell MakeHolder()'s return type auto, but not me. I think it's important to know what you're going to get. For example, otherwise you have to read the code to know what the result's ownership semantics are! Is it a raw pointer? Something else?)
Is the newly created object holding a reference to something that no longer exists?
No. Holder stores the vector by value so it's vector and the function local one are different objects.
Or is this just a more efficient way of copying the vector, in which case the fact that it was allocated on the stack makes no difference?
Yes. if
Holder(vector<int>& vref) : vec(vref) {}
had been
Holder(vector<int> vref) : vec(vref) {}
Then you would first have to copy the vector into vref and then you would have to copy vref into vec. By taking a reference you save that first copy.
Another way to do it would be
Holder(const vector<int>& vref) : vec(vref) {}
// or
Holder(vector<int> vref) : vec(std::move(vref)) {}
which allows you to accept lvalues and rvalues.
Its member variable is a vector, not a reference, so it is a Holder's own copy (and each std::vector privately owns its elements.)
Is the newly created object holding a reference to something that no longer exists?
No, copy constructor called for initialization of member variable vec
Or is this just a more efficient way of copying the vector, in which case the fact that it was allocated on the stack makes no difference?
Yes, but common practice is to use const reference to avoid copy ctor on passing argument by value:
Holder(cont vector<int>& vref) : vec(vref) {}
so maybe somebody made a mistake and missed const or had some other reason not to use const reference here.
Note: with move semantics passing object by (const) reference in some cases could be even less efficient than passing by value.

Should a std::string class member be a pointer?

And why/why not?
Say I have a class which takes a string in the constructor and stores it. Should this class member be a pointer, or just a value?
class X {
X(const std::string& s): s(s) {}
const std::string s;
};
Or...
class X {
X(const std::string* s): s(s) {}
const std::string* s;
};
If I was storing a primitive type, I'd take a copy. If I was storing an object, I'd use a pointer.
I feel like I want to copy that string, but I don't know when to decide that. Should I copy vectors? Sets? Maps? Entire JSON files...?
EDIT:
Sounds like I need to read up on move semantics. But regardless, I'd like to make my question a little more specific:
If I have a 10 megabyte file as a const string, I really don't want to copy that.
If I'm newing up 100 objects, passing a 5 character const string into each one's constructor, none of them ought to have ownership. Probably just take a copy of the string.
So (assuming I'm not completely wrong) it's obvious what to do from outside the class, but when you're designing class GenericTextHaver, how do you decide the method of text-having?
If all you need is a class that takes a const string in its constructor, and allows you to get a const string with the same value out of it, how do you decide how to represent it internally?
Should a std::string class member be a pointer?
No
And why not?
Because std::string, like every other object in the standard library, and every other well-written object in c++ is designed to be treated as a value.
It may or may not use pointers internally - that is not your concern. All you need to know is that it's beautifully written and behaves extremely efficiently (actually more efficient than you can probably imagine right now) when treated like a value... particularly if you use move-construction.
I feel like I want to copy that string, but I don't know when to decide that. Should I copy vectors? Sets? Maps? Entire JSON files...?
Yes. A well-written class has "value semantics" (this means it's designed to be treated like a value) - therefore copied and moved.
Once upon a time, when I was first writing code, pointers were often the most efficient way to get a computer to do something quickly. These days, with memory caches, pipelines and prefetching, copying is almost always faster. (yes, really!)
In a multi-processor environment, copying is very much faster in all but the most extreme cases.
If I have a 10 megabyte file as a const string, I really don't want to copy that.
If you need a copy of it, then copy it. If you really just mean to move it, then std::move it.
If I'm newing up 100 objects, passing a 5 character const string into each one's constructor, none of them ought to have ownership. Probably just take a copy of the string.
A 5-character string is so cheap to copy that you should not even think about it. Just copy it. Believe it or not, std::string is written with the full knowledge that most strings are short, and they're often copied. There won't even be any memory allocation involved.
So (assuming I'm not completely wrong) it's obvious what to do from outside the class, but when you're designing class GenericTextHaver, how do you decide the method of text-having?
Express the code in the most elegant way you can that succinctly conveys your intent. Let the compiler make decisions about how the machine code will look - that it's job. Hundreds of thousands of people have given their time to ensure that it does that job better than you ever will.
If all you need is a class that takes a const string in its constructor, and allows you to get a const string with the same value out of it, how do you decide how to represent it internally?
In almost all cases, store a copy. If 2 instances actually need to share the same string then consider something else, like a std::shared_ptr. But in that case, they probably would not only need to share a string so the 'shared state' should be encapsulated in some other object (ideally with value semantics!)
OK, stop talking - show me how the class should look
class X {
public:
// either like this - take a copy and move into place
X(std::string s) : s(std::move(s)) {}
// or like this - which gives a *miniscule* performance improvement in a
// few corner cases
/*
X(const std::string& s) : s(s) {} // from a const ref
X(std::string&& s) : s(std::move(s)) {} // from an r-value reference
*/
// ok - you made _s const, so this whole class is now not assignable
const std::string s;
// another way is to have a private member and a const accessor
// you will then be able to assign an X to another X if you wish
/*
const std::string& value() const {
return s;
}
private:
std::string s;
*/
};
If the constructor truly "takes a string and stores it", then of course your class needs to contain a std::string data member. A pointer would only point at some other string that you don't actually own, let alone "store":
struct X
{
explicit X(std::string s) : s_(std::move(s)) {}
std::string s_;
};
Note that since we're taking ownership of the string, we may as well take it by value and then move from the constructor argument.
In most cases you will want to be copying by value. If the std::string gets destroyed outside of X, X will not know about it and result in undesired behavior. However, if we want to do this without taking any copies, a natural thing to do might be to use std::unique_ptr<std::string> and use the std::move operator on it:
class X {
public:
std::unique_ptr<std::string> m_str;
X(std::unique_ptr<std::string> str)
: m_str(std::move(str)) { }
}
By doing this, note that the original std::unique_ptr will be empty. The ownership of the data has been transferred. The nice thing about this is that it protects the data without needing the overhead of a copy.
Alternately, if you still want it accessible from the outside world, you can use an std::shared_ptr<std::string> instead, though in this case care must be taken.
Yes, generally speaking, it is fine to have a class that holds a pointer to an object but you will need to implement a more complex behaviour in order to make your class safe. First, as one of the previous responders noticed it is dangerous to keep a pointer to the outside string as it can be destroyed without the knowledge of the class X. This means that the initializing string must be copied or moved when an instance of X is constructed. Secondly, since the member X.s now points to the string object allocated on the heap (with operator new), the class X needs a destructor to do the proper clean-up:
class X {
public:
X(const string& val) {
cout << "copied " << val << endl;
s = new string(val);
}
X(string&& val) {
cout << "moved " << val << endl;
s = new string(std::move(val));
}
~X() {
delete s;
}
private:
const string *s;
};
int main() {
string s = "hello world";
X x1(s); // copy
X x2("hello world"); // move
return 0;
}
Note, that theoretically you can have a constructor that takes a const string* as well. It will require more checks (nullptr), will support only copy semantics, and it may look as follows:
X(const string* val) : s(nullptr) {
if(val != nullptr)
s = new string(*val);
}
These are techniques. When you design your class the specifics of the problem at hand will dictate whether to have a value or a pointer member.

Pass a std::vector v to the ctor of a class which needs to access v during the whole life time of a corresponding instance

I want to pass a std::vector<double> v to the constructor of a class A which needs to access v during the whole life time of a corresponding instance. Since v is assumed to be huge, I don't want to make a copy of v.
We could do the following: Option 1:
class A
{
public:
A(std::vector<double> const& v)
: m_v(v)
{ }
private:
std::vector<double> const& m_v
};
This might be a suitable option if we could guarantee that the life time of the referenced v object is at least as long as the life time of the corresponding instance of A. But it's unlikely that we can guarantee that.
Option 2:
class A
{
public:
A(std::shared_ptr<std::vector<double>> v)
: m_v(v)
{ }
private:
std::shared_ptr<std::vector<double>> m_v;
};
This option has no life time issues. However, I'm not sure if this is really best practice. So, how should we do that?
If you cannot guarantee that the life time of the referenced v object is at least as long as the life time of the corresponding instance of A,
and if you don't want to copy v,
then shared_ptr is the way to go. It is considered best practice.
For example, see Guru Of The Week 91, by Herb Sutter
Note: shared_ptrwill extend v's lifetime, while weak_ptr won't - you may lose v (in a safe way) during A's life. Whether you need one or the other depends of your use case.
If the vector has to stay alive for the whole lifetime of an instance of A I would go for option 2, otherwise I would consider std::weak_ptr<std::vector<double>>
This might be a suitable option if we could guarantee that the life time of the referenced v object is at least as long as the life time of the corresponding instance of A. But it's unlikely that we can guarantee that.
(emphasis mine) suggests there is more work to do on the design. Either your class controls the lifetime of the vector or it depends on it. Which is it? This should be clear in any documentation and descriptions of the interface.
IF you stipulate that A depends on the vector being available, then it's acceptable to demand that in the interface:
A::A(const std::vector<double>& v) // Demand that v outlives A
: _v(v) {}
IF you stipulate that A will share the vector with other objects, then demand it in the interface:
A::A(std::shared_ptr<const std::vector<double>> pv) // demand shared ownswership
: _pv(std::move(pv)) {}
IF you stipulate that A will actually control the lifetime of the vector (and thereafter own it) then either move the vector into it or move a unique_ptr into A:
// call with auto a = A(std::move(v));
// or auto a = A(std::vector<double>(...));
A::A(std::vector<double> v) // demand ownership or copy
: _v(std::move(v)) {}
// or
A::A(std::unique_ptr<const std::vector<double>> pv) // demand ownership
: _pv(std::move(pv)) {}
part of 'best practice' is using interface definitions to provide guarantees to clients and make demands of them.
However, I'm not sure if this is really best practice. So, how should we do that?
auto make_huge_vector()
{
return std::make_shared<std::vector<double>>( /* ... */ );
}
Current best practice (forcefully extends lifetime of the vector, if needed):
class A
{
public:
A(std::shared_ptr<std::vector<double>> v): v_{ std::move(v) } {}
private:
std::shared_ptr<std::vector<double>> v_;
};
// client code:
auto v = make_huge_vector();
A a{v};

What lasts after using std::move c++11

After using std::move in a variable that might be a field in a class like:
class A {
public:
vector<string>&& stealVector() {
return std::move(myVector);
}
void recreateMyVector() {
}
private:
vector<string> myVector;
};
How would I recreate the vector, like a clear one? What is left in myVector after the std::move?
The common mantra is that a variable that has been "moved-from" is in a valid, but unspecified state. That means that it is possible to destroy and to assign to the variable, but nothing else.
(Stepanov calls this "partially formed", I believe, which is a nice term.)
To be clear, this isn't a strict rule; rather, it is a guideline on how to think about moving: After you move from something, you shouldn't want to use the original object any more. Any attempt to do something non-trivial with the original object (other than assigning to it or destroying it) should be carefully thought about and justified.
However, in each particular case, there may be additional operations that make sense on a moved-from object, and it's possible that you may want to take advantage of those. For example:
The standard library containers describe preconditions for their operations; operations with no pre­conditions are fine. The only useful ones that come to mind are clear(), and perhaps swap() (but prefer assignment rather than swapping). There are other operations without preconditions, such as size(), but following the above reasoning, you shouldn't have any business inquiring after the size of an object which you just said you didn't want any more.
The unique_ptr<T, D> guarantees that after being moved-from, it is null, which you can exploit in a situation where ownership is taken conditionally:
std::unique_ptr<T> resource(new T);
std::vector<std::function<int(std::unique_ptr<T> &)> handlers = /* ... */;
for (auto const & f : handlers)
{
int result = f(resource);
if (!resource) { return result; }
}
A handler looks like this:
int foo_handler(std::unique_ptr<T> & p)
{
if (some_condition))
{
another_container.remember(std::move(p));
return another_container.state();
}
return 0;
}
It would have been possible generically to have the handler return some other kind of state that indi­cates whether it took ownership from the unique pointer, but since the standard actually guaran­tees that moving-from a unique pointer leaves it as null, we can exploit that to transmit that information in the unique pointer itself.
Move the member vector to a local vector, clear the member, return the local by value.
std::vector<string> stealVector() {
auto ret = std::move(myVector);
myVector.clear();
return ret;
}
What is left in myVector after the std::move?
std::move doesn't move, it is just a cast. It can happen that myVector is intact after the call to stealVector(); see the output of the first a.show() in the example code below. (Yes, it is a silly but valid code.)
If the guts of myVector are really stolen (see b = a.stealVector(); in the example code), it will be in a valid but unspecified state. Nevertheless, it must be assignable and destructible; in case of std::vector, you can safely call clear() and swap() as well. You really should not make any other assumptions concerning the state of the vector.
How would I recreate the vector, like a clear one?
One option is to simply call clear() on it. Then you know its state for sure.
The example code:
#include <initializer_list>
#include <iostream>
#include <string>
#include <vector>
using namespace std;
class A {
public:
A(initializer_list<string> il) : myVector(il) { }
void show() {
if (myVector.empty())
cout << "(empty)";
for (const string& s : myVector)
cout << s << " ";
cout << endl;
}
vector<string>&& stealVector() {
return std::move(myVector);
}
private:
vector<string> myVector;
};
int main() {
A a({"a", "b", "c"});
a.stealVector();
a.show();
vector<string> b{"1", "2", "3"};
b = a.stealVector();
a.show();
}
This prints the followings on my machine:
a b c
(empty)
Since I feel Stepanov has been misrepresented in the answers so far, let me add a quick overview of my own:
For std types (and only those), the standard specifies that a moved-from object is left in the famous "valid, but unspecified" state. In particular, none of the std types use Stepanov's Partially-Formed State, which some, me included, think of as a mistake.
For your own types, you should strive for both the default constructor as well as the source object of a move to establish the Partially-Formed State, which Stepanov defined in Elements of Programming (2009) as a state in which the only valid operations are destruction and assignment of a new value. In particular, the Partially-Formed State need not represent a valid value of the object, nor does it need to adhere to normal class invariants.
Contrary to popular belief, this is nothing new. The Partially-Formed State exists since the dawn of C/C++:
int i; // i is Partially-Formed: only going out of scope and
// assignment are allowed, and compilers understand this!
What this practically means for the user is to never assume you can do more with a moved-from object than destroy it or assign a new value to it, unless, of course, the documentation states that you can do more, which is typically possible for containers, which can often naturally, and efficiently, establish the empty state.
For class authors, it means that you have two choices:
First, you avoid the Partially-Formed State as the STL does. But for a class with Remote State, e.g. a pimpl'ed class, this means that to represent a valid value, either you accept nullptr as a valid value for pImpl, prompting you to define, at the public API level, what a nullptr pImpl means, incl. checking for nullptr in all member functions.
Or you need to allocate a new pImpl for the moved-from (and default-constructed) object, which, of course, is nothing any performance-conscious C++ programmer would do. A performance-conscious C++ programmer, however, would also not like to litter his code with nullptr checks just to support the minor use-case of a non-trivial use of a moved-from object.
Which brings us to the second alternative: Embrace the Partially-Formed State. That means, you accept nullptr pImpl, but only for default-constructed and moved-from objects. A nullptr pImpl represents the Partially-Formed State, in which only destruction and assignment of another value are allowed. This means that only the dtor and the assignment operators need to be able to deal with a nullptr pImpl, while all other members can assume a valid pImpl. This has another benefit: both your default ctor as well as the move operators can be noexcept, which is important for use in std::vector (so moves and not copies are used upon reallocation).
Example Pen class:
class Pen {
struct Private;
Private *pImpl = nullptr;
public:
Pen() noexcept = default;
Pen(Pen &&other) noexcept : pImpl{std::exchange(other.pImpl, {})} {}
Pen(const Pen &other) : pImpl{new Private{*other.pImpl}} {} // assumes valid `other`
Pen &operator=(Pen &&other) noexcept {
Pen(std::move(other)).swap(*this);
return *this;
}
Pen &operator=(const Pen &other) {
Pen(other).swap(*this);
return *this;
}
void swap(Pen &other) noexcept {
using std::swap;
swap(pImpl, other.pImpl);
}
int width() const { return pImpl->width; }
// ...
};

Using a class with const data members in a vector

Given a class like this:
class Foo
{
const int a;
};
Is it possible to put that class in a vector? When I try, my compiler tells me it can't use the default assignment operator. I try to write my own, but googling around tells me that it's impossible to write an assignment operator for a class with const data members. One post I found said that "if you made [the data member] const that means you don't want assignment to happen in the first place." This makes sense. I've written a class with const data members, and I never intended on using assignment on it, but apparently I need assignment to put it in a vector. Is there a way around this that still preserves const-correctness?
I've written a class with const data members, and I never intended on using assignment on it, but apparently I need assignment to put it in a vector. Is there a way around this that still preserves const-correctness?
You have to ask whether the following constraint still holds
a = b;
/* a is now equivalent to b */
If this constraint is not true for a and b being of type Foo (you have to define the semantics of what "equivalent" means!), then you just cannot put Foo into a Standard container. For example, auto_ptr cannot be put into Standard containers because it violates that requirement.
If you can say about your type that it satisfies this constraint (for example if the const member does not in any way participate to the value of your object, but then consider making it a static data member anyway), then you can write your own assignment operator
class Foo
{
const int a;
public:
Foo &operator=(Foo const& f) {
/* don't assign to "a" */
return *this;
}
};
But think twice!. To me, it looks like that your type does not satisfy the constraint!
Use a vector of pointers std::vector<Foo *>. If you want to avoid the hassle of cleaning up after yourself, use boost::ptr_vector.
Edit: My initial stab during my coffee break, static const int a; won't work for the use case the OP has in mind, which the initial comments confirm, so I'm rewriting and expanding my answer.
Most of the time, when I want to make an element of a class constant, it's a constant whose value is constant for all time and across all instances of the class. In that case, I use a static const variable:
class Foo
{
public:
static const int a;
};
Those don't need to be copied among instances, so if it applied, that would fix your assignment problem. Unfortunately, the OP has indicated that this won't work for the case the OP has in mind.
If you want to create a read-only value that clients can't modify, you can make it a private member variable and only expose it via a const getter method, as another post on this thread indicates:
class Foo
{
public:
int get_a() const { return a; }
private:
int a;
};
The difference between this and
class Foo
{
public:
const int a;
};
is:
The const int gives you assurance that not even the implementation of the class will be able to muck with the value of a during the lifetime of the object. This means that assignment rightfully won't work, since that would be trying to modify the value of a after the object's been created. (This is why, btw, writing a custom operator=() that skips the copy of a is probably a bad idea design-wise.)
The access is different – you have to go through a getter rather than accessing the member directly.
In practice, when choosing between the two, I use read-only members. Doing so probably means you'll be able to replace the value of an object with the value of another object without violating semantics at all. Let's see how it would work in your case.
Consider your Grid object, with a width and height. When you initially create the vector, and let's say you reserve some initial space using vector::reserve(), your vector will be populated with initial default-initialized (i.e. empty) Grids. When you go to assign to a particular position in the vector, or push a Grid onto the end of the vector, you replace the value of the object at that position with a Grid that has actual stuff. But you may be OK with this! If the reason you wanted width and height to be constant is really to ensure consistency between width and height and the rest of the contents of your Grid object, and you've verified that it doesn't matter whether width and height are replaced before or after other elements of Grid are replaced, then this assignment should be safe because by the end of the assignment, the entire contents of the instance will have been replaced and you'll be back in a consistent state. (If the lack of atomicity of the default assignment was a problem, you could probably get around this by implementing your own assignment operator which used a copy constructor and a swap() operation.)
In summary, what you gain by using read-only getters is the ability to use the objects in a vector or any container with value semantics. However, it then falls to you to ensure that none of Grid's internal operations (or the operations of friends of Grid) violate this consistency, because the compiler won't be locking down the width and height for you. This goes for default construction, copy construction, and assignment as well.
I'm considering making the data member non-const, but private and only accessible by a get function, like this:
class Foo
{
private:
int a;
public:
int getA() const {return a;}
};
Is this 'as good' as const? Does it have any disadvantages?
As of c++20, using const member variables are legal without restrictions that had made it virtually unusable in containers. You still have to define a copy assignment member function because it continues to be automatically deleted when a const object exists in the class. However, changes to "basic.life" now allow changing const sub-objects and c++ provides rather convenient functions for doing this. Here's a description of why the change was made:
The following code shows how to define a copy assignment member function which is useable in any class containing const member objects and uses the new functions std::destroy_at and std::construct_at to fulfil the requirement so the new "basic.life" rules. The code demonstrates assignment of vectors as well as sorting vectors with const elements.
Compiler explorer using MSVC, GCC, CLANG https://godbolt.org/z/McfcaMWqj
#include <memory>
#include <vector>
#include <iostream>
#include <algorithm>
class Foo
{
public:
const int a;
Foo& operator=(const Foo& arg) {
if (this != &arg)
{
std::destroy_at(this);
std::construct_at(this, arg);
}
return *this;
}
};
int main()
{
std::vector<Foo> v;
v.push_back({ 2 });
v.push_back({ 1 });
v.insert(v.begin() + 1, Foo{ 0 });
std::vector<Foo> v2;
v2 = v;
std::sort(v2.begin(), v2.end(), [](auto p1, auto p2) {return p1.a < p2.a; });
for (auto& x : v2)
std::cout << x.a << '\n';
}