C ++:STL使用const类成员时遇到麻烦

时间:2010-07-30 14:35:28

标签: c++

这是一个开放式的问题。 有效的C ++。第3项。尽可能使用const。真的吗?

我想做一些在对象生命周期const期间不会改变的东西。但const带来了它自己的麻烦。如果类具有任何const成员,则禁用编译器生成的赋值运算符。如果没有赋值运算符,则类将无法与STL一起使用。如果您想提供自己的赋值运算符,则需要 const_cast 。这意味着更多的喧嚣和更多的错误空间。你经常使用const类成员吗?

编辑:作为一项规则,我努力保持const正确性,因为我做了很多多线程。我很少需要为我的类实现复制控制,从不编写删除代码(除非绝对必要)。我觉得const的当前状态与我的编码风格相矛盾。 Const迫使我实现赋值运算符,即使我不需要它。即使没有 const_cast 分配也很麻烦。您需要确保所有const成员比较相等,然后手动复制所有非const成员。

代码。希望它能澄清我的意思。您在下面看到的课程不适用于STL。您需要为它实现一个赋值,即使您不需要它。

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

15 个答案:

答案 0 :(得分:18)

你自己说过你把const变成“在物体生命期间不会改变的东西”。然而,您抱怨隐式声明的赋值运算符被禁用。但隐式声明赋值运算符 会更改有问题成员的内容!完全合乎逻辑(根据您自己的逻辑),它正在被禁用。要么是,要么你不应该声明成员const。

此外,为您提供自己的赋值运算符不需要const_cast。为什么?您是否正在尝试分配给您在赋值运算符中声明为const的成员?如果是这样,为什么你声明它为const呢?

换句话说,提供您正在遇到的问题的更有意义的描述。到目前为止你提供的那个以最明显的方式是自相矛盾的。

答案 1 :(得分:5)

我很少使用它们 - 麻烦太大了。当然,在成员函数,参数或返回类型方面,我总是力求const正确性。

答案 2 :(得分:5)

正如AndreyT指出的那样,在这些情况下,分配(大多数)并没有多大意义。问题是vector(例如)是该规则的一个例外。

逻辑上,您将对象复制到vector,稍后您将获得原始对象的另一个副本。从纯粹的逻辑观点来看,不涉及任务。问题是vector要求对象无论如何都是可分配的(实际上,所有C ++容器都可以)。它基本上是一个实现细节(在代码中的某个地方,它可能分配对象而不是复制它们)的一部分接口。

没有简单的治疗方法。即使定义自己的赋值运算符并使用const_cast也无法解决问题。当您获得const_cast指针或对您知道实际上未定义为const的对象的引用时,使用const是完全安全的。但是,在这种情况下,变量本身 被定义为const - 试图抛弃const并分配给它会给出未定义的行为。实际上,它几乎总是可以工作(只要它不是static const,并且在编译时已经知道了初始化程序),但是不能保证它。

C ++ 11和更新版本为这种情况添加了一些新的曲折。特别是,不再需要需要的对象可以存储在向量(或其他集合)中。它们可以移动就足够了。这在这种特殊情况下没有帮助(移动const对象比分配它更容易)但是在其他一些情况下确实使生活变得更加容易(例如,肯定有类型可移动但是不可转让/可复制的。)

在这种情况下,可以通过添加间接级别来使用移动而不是副本。如果你创建一个“外部”和一个“内部”对象,内部对象中有const成员,外部对象只包含指向内部的指针:

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

    inner *i;
};

...然后当我们创建outer的实例时,我们定义一个inner对象来保存const数据。当我们需要做一个赋值时,我们做一个典型的移动赋值:将指针从旧对象复制到新对象,并且(可能)将旧对象中的指针设置为nullptr,所以当它被销毁时,它就赢了试图摧毁内部物体。

如果你想要足够严重,你可以在旧版本的C ++中使用(排序)相同的技术。您仍然使用外部/内部类,但每个赋值将分配一个全新的内部对象,或者您使用类似shared_ptr的东西让外部实例共享对单个内部对象的访问权限,并在最后一个外部物体被摧毁。

它没有任何真正的区别,但至少对于管理向量时使用的赋值,只有inner的两个引用,而vector正在调整自身大小(调整大小)这就是为什么一个向量需要赋值可以开始的。)

答案 3 :(得分:4)

编译时的错误很痛苦,但运行时的错误是致命的。使用const的构造可能是代码的麻烦,但它可能会帮助您在实现它们之前找到错误。我尽可能使用consts。

答案 4 :(得分:3)

我尽可能遵循尽可能使用const的建议,但我同意,当涉及到班级成员时,const是一个很大的麻烦。

我发现我对const非常小心 - 在参数方面是正确的,但对于类成员则没有那么多。实际上,当我创建类成员const并导致错误(由于使用STL容器)时,我要做的第一件事就是删除const

答案 5 :(得分:3)

我想知道你的情况......以下所有内容都是假设,因为你没有提供描述你问题的示例代码,所以......

原因

我猜你有类似的东西:

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

IIRC,默认赋值运算符将执行逐个成员的赋值,类似于:

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

因此,这不会生成。

所以,你的问题是如果没有这个赋值运算符,STL容器将不接受你的对象。

就我所见:

  1. 编译器不生成此operator =
  2. 是正确的
  3. 你应该提供自己的,因为只有你知道你想要什么
  4. 您的解决方案

    我害怕明白const_cast是什么意思。

    我自己的问题解决方案是编写以下用户定义的运算符:

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

    这样,如果你有:

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

    据我所知,它是连贯的。但我想,在阅读const_cast解决方案时,您希望有更多类似的内容:

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

    这意味着operator =的以下代码:

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

    但是,你在问题中写道:

      

    我想制作在对象生命周期期间不会改变的任何东西

    我认为是你自己的const_cast解决方案,k在对象生命周期中发生了变化,这意味着你自相矛盾,因为你需要一个在对象生命期内不会改变的成员变量除非你想改变

    解决方案

    接受您的成员变量在其所有者对象的生命周期内将更改的事实,并删除co​​nst。

答案 6 :(得分:2)

如果您想保留shared_ptr成员,可以将const存储到STL容器中的const个对象。

#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;
}
但是,正如其他人已经指出的那样,如果您希望编译器生成复制构造函数,那么在我看来删除const限定成员通常会更容易麻烦。

答案 7 :(得分:1)

我只在引用或指针类成员上使用const。我用它来表明不应该改变引用或指针的目标。如你所知,在其他类别的成员上使用它是一件很麻烦的事。

使用const的最佳位置是函数参数,各种指针和引用,常量整数和临时便利值。

临时便利变量的一个例子是:

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

那个buf_end指针永远不应该指向其他地方,所以使它成为常量是一个好主意。与len相同的想法。如果buf内的字符串从未在函数的其余部分中发生更改,则其len也不应更改。如果可以的话,我甚至会在调用buf后将fill_buf更改为const,但C / C ++不会让你这样做。

答案 8 :(得分:1)

关键是海报希望在其实施中保护const,但仍希望对象可分配。该语言不方便地支持这种语义,因为成员的constness位于相同的逻辑级别并且与可赋值性紧密耦合。

然而,带有引用计数实现或智能指针的pImpl成语将完全符合海报的要求,因为可转移性随后被移出实现并向更高级别的对象上升。实现对象仅被构造/破坏,从而在较低级别永远不需要赋值。

答案 9 :(得分:0)

我认为你的陈述

  

如果一个类有const任何成员,那么   编译生成的赋值运算符   被禁用。

可能不对。我有使用const方法的类

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

也用于STL。那么您的观察可能依赖于编译器还是仅适用于成员变量?

答案 10 :(得分:0)

这不是太难。制作自己的赋值运算符不会有任何问题。不需要分配const位(因为它们是const)。

<强>更新
关于const的含义存在一些误解。这意味着它永远不会改变。

如果一个赋值应该改变它,那么它不是const。 如果您只是想阻止其他人更改它,请将其设为私有,并且不提供更新方法 结束更新

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;
}

答案 11 :(得分:0)

如果类本身是不可复制的,我只会使用const成员。我用boost :: noncopyable

声明了很多类
class Foo : public boost::noncopyable {
    const int x;
    const int y;
}

然而,如果你想要非常偷偷摸摸并且给自己带来很多潜力 你可以在没有作业的情况下影响复制结构的问题 要小心点。

#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;

}

输出

1
2
1
1
已使用placement new运算符将

foo复制到bar。

答案 12 :(得分:0)

从哲学上讲,它看起来像安全性能权衡。 Const用于安全。据我所知,容器使用赋值来重用内存,即为了性能。他们可能会使用显式销毁和替换新的替代(并且逻辑上它更正确),但是赋值有更高效的机会。我想,逻辑上冗余的要求是“可分配”(复制可构造就足够了),但是stl容器希望更快更简单。

当然,可以将赋值实现为显式destroy + placement new以避免const_cast hack

答案 13 :(得分:0)

与其声明数据成员 const,您还可以创建类 const 的公共表面,除了隐式定义的部分使其成为 (semi){{3 }}。

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

答案 14 :(得分:-1)

你基本上不想把const成员变量放在一个类中。 (同意使用引用作为类的成员。)

Constness真正用于程序的控制流程 - 防止在代码中错误的时间发生变异。因此,不要在类的定义中声明const成员变量,而是在声明类的实例时全部或全部使用它。