繁体   English   中英

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

[英]C++: STL troubles with const class members

这是一个开放式的问题。 有效的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_;
};

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

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

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

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

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

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

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

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

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

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

    inner *i;
};

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

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

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

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

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

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

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

原因

我想你有类似的东西:

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. 你应该提供自己的,因为只有你知道你想要什么

你解决方案

我害怕明白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期间不会改变的东西

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

解决方案

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

如果您想保留const成员,可以将shared_ptr存储到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限定成员往往更容易。

我只在引用或指针类成员上使用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指针永远不应该指向其他任何地方,所以使它成为const是一个好主意。 len相同的想法。 如果buf的字符串在函数的其余部分中永远不会改变,那么它的len也不应该改变。 如果可以,我甚至会在调用fill_buf之后将buf更改为const,但C / C ++不会让你这样做。

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

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

我想你的说法

如果类具有const任何成员,则禁用编译器生成的赋值运算符。

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

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

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

这不是太难。 制作自己的赋值运算符不会有任何问题。 不需要分配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;
}

我只会使用const成员iff类本身是不可复制的。 我用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。

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

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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