Using a class with const data members in a vector
Asked Answered
C

5

12

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?

Carpogonium answered 26/5, 2010 at 22:20 Comment(2)
Why exactly do you need a const member? What is Foo really?Krys
This is now doable with c++20 using destroy_at and construct_at to modify the values of const members of an object in a vector w/o UB.Montgolfier
N
8

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!

Novercal answered 26/5, 2010 at 22:25 Comment(2)
So you're saying if I'm sure I'll never write a = b with my type, then it's okay to write an assignment operator that does nothing? Makes sense to me, but what makes you think my type does not satisfy that constraint? -EDIT- On second thought, I see what you're getting at better. I agree that my type does not satisfy the constraint.Carpogonium
@Max: That's not what he's saying. He's saying do all the assignment like normal, but ignore the assignment of the const member. And the container is free to do a = b, it has nothing to do with what you'll do with it.Hauteur
L
3

Use a vector of pointers std::vector<Foo *>. If you want to avoid the hassle of cleaning up after yourself, use boost::ptr_vector.

Levorotation answered 26/5, 2010 at 22:23 Comment(0)
B
1

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.

Bonnette answered 26/5, 2010 at 22:25 Comment(3)
Making a variable static makes all instances of that class share it. If you actually need the constant to be unique to instances then this will definitely not work at all.Epigraphy
Hm, I might have misunderstood the use case. Is this a constant that needs to vary from instance to instance? I haven't yet run into the case where I've really needed one of those.Bonnette
It is. In the program I'm writing, I have a Grid class, that has width and height variables. I never want the width and height to change once their initialized, so I make them constant, but I do want Grids of various sizes in a vector.Carpogonium
M
1

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';
}
Montgolfier answered 22/4, 2022 at 21:10 Comment(0)
C
0

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?

Carpogonium answered 26/5, 2010 at 22:48 Comment(1)
We have no idea what you're doing with your data. but if it was originally const so clients of the class couldn't modify it, yes. This is known as encapsulation; make data private and expose it through methods.Hauteur

© 2022 - 2024 — McMap. All rights reserved.