代码之家  ›  专栏  ›  技术社区  ›  user401947

C++:STL类成员的STL问题

c++
  •  13
  • user401947  · 技术社区  · 15 年前

    有效的C++。项目3。 真的?

    我想做任何在对象生命周期内不变的东西。但康斯特也有自己的麻烦。如果类有任何常量成员,编译器生成的赋值运算符将被禁用。如果没有赋值运算符,类将无法使用STL。如果您想提供自己的赋值运算符, 施工图 是必需的。这意味着更多的匆忙和更多的错误空间。你多久使用一次const类成员?

    编辑: 作为一个规则,我争取const的正确性,因为我做了很多多线程处理。我很少需要为我的类实现复制控制,也从不进行代码删除(除非绝对必要)。我觉得const的现状与我的编码风格相矛盾。Const强制我实现赋值操作符,即使我不需要。即使没有 任务很麻烦。您需要确保所有常量成员比较相等,然后手动复制所有非常量成员。

    代码。

    class Multiply {
    public:
        Multiply(double coef) : coef_(coef) {}
        double operator()(double x) const {
            return coef_*x;
        }
    private:
        const double coef_;
    };
    
    14 回复  |  直到 15 年前
        1
  •  5
  •   Jerry Coffin    9 年前

    正如安德烈指出的,在这种情况下,分配(大部分)没有什么意义。问题是 vector (例如)是该规则的一个例外。

    矢量 要求对象可以被分配(实际上,所有C++容器都是)。它基本上是将实现细节(在其代码的某个地方,它可能会分配对象而不是复制它们)作为接口的一部分。

    没有简单的治疗方法。甚至定义自己的赋值运算符并使用 const_cast 并不能解决问题。使用起来非常安全 施工图 当你得到一个 const 常数 . 但是,在这种情况下,变量本身 定义为 常数 --试图抛弃 不确定性和赋值给了它未定义的行为。在现实中,它几乎总是工作无论如何(只要它不是) static const 使用编译时已知的初始值设定项),但不能保证。

    C++ 11和更新的新增加了一些新的扭转这种情况。特别是,对象不再 需要 常数 但在其他一些情况下(例如,确实有一些类型是可移动的,但不可分配/复制的)。

    能够 通过添加一个间接级别来使用move而不是copy。如果您创建了一个“外部”和一个“内部”对象 常数 成员,而外部对象仅包含指向内部对象的指针:

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

    …然后当我们创建 outer inner 对象来保持 常数 数据。当我们需要执行赋值时,我们会执行一个典型的移动赋值:将指针从旧对象复制到新对象,并(可能)将旧对象中的指针设置为nullptr,这样当它被销毁时,它就不会尝试销毁内部对象。

    如果你想足够糟糕,你可以在老版本的C++中使用(排序)相同的技术。您仍然可以使用outer/inner类,但每个赋值都会分配一个全新的内部对象,或者您可以使用类似shared\u ptr的方法让外部实例共享对单个内部对象的访问,并在最后一个外部对象被销毁时将其清除。

    这并没有什么实际的区别,但至少对于管理向量时使用的赋值,只有两个引用 矢量 正在调整自身大小(调整大小是向量需要可赋值的原因)。

        2
  •  18
  •   AnT stands with Russia    15 年前

    你自己说过你让const“在对象的生命周期内不会改变的任何东西”。但是您抱怨隐式声明的赋值运算符被禁用。但隐式声明赋值运算符 更改有问题的成员的内容!它被禁用是完全合乎逻辑的(根据你自己的逻辑)。或者你不应该声明成员const。

    const_cast 分配

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

        3
  •  5
  •   anon anon    15 年前

    我很少使用它们-麻烦太大了。当然,当涉及到成员函数、参数或返回类型时,我总是力求const的正确性。

        4
  •  4
  •   Johan    15 年前

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

        5
  •  3
  •   Justin Ardini    15 年前

    我尽我最大的努力遵循使用的建议 const 常数 是个大麻烦。

    我发现我很小心 常数 常数 它会导致一个错误(由于使用STL容器),我要做的第一件事就是删除 常数 .

        6
  •  3
  •   Community CDub    5 年前

    我想知道你的案子。。。下面的一切都只是假设,因为您没有提供描述您的问题的示例代码,所以。。。

    原因

    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 =

    你的解决方案

    我不敢理解你说的是什么意思 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 } 
    

    在我看来,这是连贯的。但我想,读 施工图 解决方案,你想有更像:

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

    也就是说下面的代码 操作员= :

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

    但是,你在问题中写道:

    我想做任何在对象生命周期内不变的东西

    我以为是你自己的 解,k在对象生存期内发生了变化,这意味着你自相矛盾,因为 除非你想改变

    解决方案

        7
  •  2
  •   Sam Miller    15 年前

    你可以储存 shared_ptr 给你的 const STL容器中的对象 常数 成员。

    #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限定成员会有点麻烦,而且在我看来通常更容易。

        8
  •  1
  •   Zan Lynx    15 年前

    使用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 不改变函数的其余部分 也不应该改变。如果可以,我甚至会改变 呼叫后继续 fill_buf 但是C/C++不允许你这么做。

        9
  •  1
  •   nbourbaki    15 年前

    const 但仍希望对象可分配。该语言不方便地支持这种语义,因为成员的常量位于同一逻辑层,并且与可分配性紧密耦合。

    然而 pImpl

        10
  •  0
  •   John Rocha    15 年前

    我认为你的陈述

    编译器生成的赋值运算符 已禁用。

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

    也用于STL。那么,您的观察结果可能依赖于编译器,或者只适用于成员变量?

        11
  •  0
  •   bradgonesurfing    15 年前

    如果类本身是不可复制的,我只会使用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
    

        12
  •  0
  •   Sliq    15 年前

    这并不难。你不应该有任何困难,使你自己的赋值运算符。常量位不需要分配(因为它们是常量)。

    更新

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

    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;
    }
    
        13
  •  0
  •   user396672    15 年前

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

    当然,可以将赋值实现为显式销毁+新放置,以避免const\u cast hack

        14
  •  -1
  •   Ken Bloom    15 年前

    你基本上不想把一个常量成员变量放到一个类中(使用引用作为类的成员也是如此。)