简体   繁体   中英

C++: STL troubles with const class members

It is an open ended question. Effective C++. Item 3. Use const whenever possible. Really?

I would like to make anything which doesn't change during the objects lifetime const. But const comes with it own troubles. If a class has any const member, the compiler generated assignment operator is disabled. Without an assignment operator a class won't work with STL. If you want to provide your own assignment operator, const_cast is required. That means more hustle and more room for error. How often you use const class members?

EDIT: As a rule, I strive for const correctness because I do a lot of multithreading. I rarely need to implemented copy control for my classes and never code delete (unless it is absolutely necessary). I feel that the current state of affairs with const contradicts my coding style. Const forces me to implement assignment operator even though I don't need one. Even without const_cast assignment is a hassle. You need to make sure that all const members compare equal and then manually copy all non-const member.

Code. Hope it will clarify what I mean. The class you see below won't work with STL. You need to implement an assignment for it, even though you don't need one.

class Multiply {
public:
    Multiply(double coef) : coef_(coef) {}
    double operator()(double x) const {
        return coef_*x;
    }
private:
    const double coef_;
};

You said yourself that you make const "anything which doesn't change during the objects lifetime". Yet you complain about the implicitly declared assignment operator getting disabled. But implicitly declared assignment operator does change the contents of the member in question! It is perfectly logical (according to your own logic) that it is getting disabled. Either that, or you shouldn't be declaring that member const.

Also, providing you own assignment operator does not require a const_cast . Why? Are you trying to assign to the member you declared const inside your assignment operator? If so, why did you declare it const then?

In other words, provide a more meaningful description of the problems you are running into. The one you provided so far is self-contradictory in the most obvious manner.

I very rarely use them - the hassle is too great. Of course I always strive for const correctness when it comes to member functions, parameters or return types.

As AndreyT pointed out, under these circumstances assignment (mostly) doesn't make a lot of sense. The problem is that vector (for one example) is kind of an exception to that rule.

Logically, you copy an object into the vector , and sometime later you get back another copy of the original object. From a purely logical viewpoint, there's no assignment involved. The problem is that vector requires that the object be assignable anyway (actually, all C++ containers do). It's basically making an implementation detail (that somewhere in its code, it might assign the objects instead of copying them) part of the interface.

There is no simple cure for this. Even defining your own assignment operator and using const_cast doesn't really fix the problem. It's perfectly safe to use const_cast when you get a const pointer or reference to an object that you know isn't actually defined to be const . In this case, however, the variable itself is defined to be const -- attempting to cast away the const ness and assign to it gives undefined behavior. In reality, it'll almost always work anyway (as long as it's not static const with an initializer that's known at compile time), but there's no guarantee of it.

C++ 11 and newer add a few new twists to this situation. In particular, objects no longer need to be assignable to be stored in a vector (or other collections). It's sufficient that they be movable. That doesn't help in this particular case (it's no easier to move a const object than it is to assign it) but does make life substantially easier in some other cases (ie, there are certainly types that are movable but not assignable/copyable).

In this case, you could use a move rather than a copy by adding a level of indirection. If your create an "outer" and an "inner" object, with the const member in the inner object, and the outer object just containing a pointer to the inner:

struct outer { 
    struct inner {
        const double coeff;
    };

    inner *i;
};

...then when we create an instance of outer , we define an inner object to hold the const data. When we need to do an assignment, we do a typical move assignment: copy the pointer from the old object to the new one, and (probably) set the pointer in the old object to a nullptr, so when it's destroyed, it won't try to destroy the inner object.

If you wanted to badly enough, you could use (sort of) the same technique in older versions of C++. You'd still use the outer/inner classes, but each assignment would allocate a whole new inner object, or you'd use something like a shared_ptr to let the outer instances share access to a single inner object, and clean it up when the last outer object is destroyed.

It doesn't make any real difference, but at least for the assignment used in managing a vector, you'd only have two references to an inner while the vector was resizing itself (resizing is why a vector requires assignable to start with).

Errors at compile time are painful, but errors at runtime are deadly. Constructions using const might be a hassle to code, but it might help you find bugs before you implement them. I use consts whenever possible.

I try my best to follow the advice of using const whenever possible, however I agree that when it comes to class members, const is a big hassle.

I have found that I am very careful with const -correctness when it comes to parameters, but not as much with class members. Indeed, when I make class members const and it results in an error (due to using STL containers), the first thing I do is remove the const .

I'm wondering about your case... Everything below is but supposition because you did not provide the example code describing your problem, so...

The cause

I guess you have something like:

struct MyValue
{
   int         i ;
   const int   k ;
} ;

IIRC, the default assignment operator will do a member-by-member assignment, which is akin to :

MyValue & operator = (const MyValue & rhs)
{
   this->i = rhs.i ;
   this->k = rhs.k ; // THIS WON'T WORK BECAUSE K IS CONST
   return *this ;
} ;

Thus, this won't get generated.

So, your problem is that without this assignment operator, the STL containers won't accept your object.

As far I as see it:

  1. The compiler is right to not generate this operator =
  2. You should provide your own, because only you know exactly what you want

You solution

I'm afraid to understand what do you mean by const_cast .

My own solution to your problem would be to write the following user defined operator :

MyValue & operator = (const MyValue & rhs)
{
   this->i = rhs.i ;
   // DON'T COPY K. K IS CONST, SO IT SHOULD NO BE MODIFIED.
   return *this ;
} ;

This way, if you'll have:

MyValue a = { 1, 2 }, b = {10, 20} ;
a = b ; // a is now { 10, 2 } 

As far as I see it, it is coherent. But I guess, reading the const_cast solution, that you want to have something more like:

MyValue a = { 1, 2 }, b = {10, 20} ;
a = b ; // a is now { 10, 20 } :  K WAS COPIED

Which means the following code for operator = :

MyValue & operator = (const MyValue & rhs)
{
   this->i = rhs.i ;
   const_cast<int &>(this->k) = rhs.k ;
   return *this ;
} ;

But, then, you wrote in your question:

I would like to make anything which doesn't change during the objects lifetime const

With what I supposed is your own const_cast solution, k changed during the object lifetime, which means that you contradict yourself because !

The solution

Accept the fact your member variable will change during the lifetime of its owner object, and remove the const.

you can store shared_ptr to your const objects in STL containers if you'd like to retain const members.

#include <iostream>

#include <boost/foreach.hpp>
#include <boost/make_shared.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/utility.hpp>

#include <vector>

class Fruit : boost::noncopyable
{
public:
    Fruit( 
            const std::string& name
         ) :
        _name( name )
    {

    }

    void eat() const { std::cout << "eating " << _name << std::endl; }

private:
    const std::string _name;
};

int
main()
{
    typedef boost::shared_ptr<const Fruit> FruitPtr;
    typedef std::vector<FruitPtr> FruitVector;
    FruitVector fruits;
    fruits.push_back( boost::make_shared<Fruit>("apple") );
    fruits.push_back( boost::make_shared<Fruit>("banana") );
    fruits.push_back( boost::make_shared<Fruit>("orange") );
    fruits.push_back( boost::make_shared<Fruit>("pear") );
    BOOST_FOREACH( const FruitPtr& fruit, fruits ) {
        fruit->eat();
    }

    return 0;
}

though, as others have pointed out it's somewhat of a hassle and often easier in my opinion to remove the const qualified members if you desire the compiler generated copy constructor.

I only use const on reference or pointer class members. I use it to indicate that the target of the reference or pointer should not be changed. Using it on other kinds of class members is a big hassle as you found out.

The best places to use const is in function parameters, pointers and references of all kinds, constant integers and temporary convenience values.

An example of a temporary convenience variable would be:

char buf[256];
char * const buf_end = buf + sizeof(buf);
fill_buf(buf, buf_end);
const size_t len = strlen(buf);

That buf_end pointer should never point anywhere else so making it const is a good idea. The same idea with len . If the string inside buf never changes in the rest of the function then its len should not change either. If I could, I would even change buf to const after calling fill_buf , but C/C++ does not let you do that.

The point is that the poster wants const protection within his implementation but still wants the object assignable. The language does not support such semantics conveniently as constness of the member resides at the same logical level and is tightly coupled with assignability.

However, the pImpl idiom with a reference counted implementation or smart pointer will do exactly what the poster wants as assignability is then moved out of the implementation and up a level to the higher level object. The implementation object is only constructed/destructed whence assignment is never needed at the lower level.

I think your statement

If a class has const any member, the compiler generated assignment operator is disabled.

Might be incorrect. I have classes that have const method

bool is_error(void) const;
....
virtual std::string info(void) const;
....

that are also used with STLs. So perhaps your observation is compiler dependent or only applicable to the member variables?

It isn't too hard. You shouldn't have any trouble making your own assignment operator. The const bits don't need to be assigned (as they're const).

Update
There is some misunderstanding about what const means. It means that it will not change, ever.

If an assignment is supposed to change it, then it isn't const. If you just want to prevent others changing it, make it private and don't provide an update method.
End Update

class CTheta
{
public:
    CTheta(int nVal)
    : m_nVal(nVal), m_pi(3.142)
    {
    }
    double GetPi() const { return m_pi; }
    int GetVal()   const { return m_nVal; }
    CTheta &operator =(const CTheta &x)
    {
        if (this != &x)
        {
            m_nVal = x.GetVal();
        }
        return *this;
    }
private:
    int m_nVal;
    const double m_pi;
};

bool operator < (const CTheta &lhs, const CTheta &rhs)
{
    return lhs.GetVal() < rhs.GetVal();
}
int main()
{
    std::vector<CTheta> v;
    const size_t nMax(12);

    for (size_t i=0; i<nMax; i++)
    {
        v.push_back(CTheta(::rand()));
    }
    std::sort(v.begin(), v.end());
    std::vector<CTheta>::const_iterator itr;
    for (itr=v.begin(); itr!=v.end(); ++itr)
    {
        std::cout << itr->GetVal() << " " << itr->GetPi() << std::endl;
    }
    return 0;
}

I would only use const member iff the class itself is non-copyable. I have many classes that I declare with boost::noncopyable

class Foo : public boost::noncopyable {
    const int x;
    const int y;
}

However if you want to be very sneaky and cause yourself lots of potential problems you can effect a copy construct without an assignment but you have to be a bit careful.

#include <new>
#include <iostream>
struct Foo {
    Foo(int x):x(x){}
    const int x;
    friend std::ostream & operator << (std::ostream & os, Foo const & f ){
         os << f.x;
         return os;
    }
};

int main(int, char * a[]){
    Foo foo(1);
    Foo bar(2);
    std::cout << foo << std::endl;
    std::cout << bar<< std::endl;
    new(&bar)Foo(foo);
    std::cout << foo << std::endl;
    std::cout << bar << std::endl;

}

outputs

1
2
1
1

foo has been copied to bar using the placement new operator.

Philosophically speaking, it looks as safety-performance tradeoff. Const used for safety. As I understand, containers use assignment to reuse memory, ie for sake of performance. They would may use explicit destruction and placement new instead (and logicaly it is more correct), but assignment has a chance to be more efficient. I suppose, it is logically redundant requirement "to be assignable" (copy constructable is enough), but stl containers want to be faster and simpler.

Of course, it is possible to implement assignment as explicit destruction+placement new to avoid const_cast hack

You basically never want to put a const member variable in a class. (Ditto with using references as members of a class.)

Constness is really intended for your program's control flow -- to prevent mutating objects at the wrong times in your code. So don't declare const member variables in your class's definition, rather make it all or nothing when you declare instances of the class.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM