对象的std::vector和常量正确性

18

考虑以下内容:

class A {
public:
    const int c; // must not be modified!

    A(int _c)
    :   c(_c)
    {
        // Nothing here
    }

    A(const A& copy)
    : c(copy.c)
    {
        // Nothing here
    }    
};



int main(int argc, char *argv[])
{
    A foo(1337);

    vector<A> vec;
    vec.push_back(foo); // <-- compile error!
    
    return 0;
}

很明显,拷贝构造函数不够。我错过了什么?

编辑:
当然,我不能在operator=()方法中更改this->c,所以我不知道operator=()会被使用(虽然std::vector要求)。


5
编译错误是什么?请具体说明,提供一个合适的测试用例!(提示:成为一名顶尖程序员,编写一个规范的、最小化的、自包含的测试用例) - Roger Pate
2
我认为你有一个选择:要么失去const,要么失去使用vector的能力。如果开始围绕这个问题进行操作,并允许operator=修改const成员,那么你现在已经给了任何代码片段执行相同操作的方法。 - UncleBens
@anand:c 只能在构造函数中设置。在我的特定情况下,我有一个指向某个父节点的指针。task * const parent; 这个指针不能被重新设置,因此在类内或外部都不允许更改 c。 - eisbaw
1
对我来说,将“const int c”作为类A的成员的选择是问题的根本原因。这就是我的问题所在。拥有operator=()并使用const_cast<>来强制转换const以分配一个值给“c”听起来像是一种修补编译器错误而不是解决实际问题的解决方案。 - yasouser
@eisbaw 你知道吗,你可以把这个变成一个指向你的类的智能指针向量。这样就可以解决你的问题,而不必更改任何定义。 - Conspicuous Compiler
显示剩余2条评论
10个回答

22

我不确定为什么没有人说出来,但正确的答案是去掉 const,或者在向量中存储 A*(使用适当的智能指针)。

通过让“复制”调用未定义行为或什么都不做(因此不是副本),可以使您的类的语义变得可怕,但为什么要围绕着UB和糟糕的代码打转呢?将其设置为const有什么好处?(提示:没有。)您的问题是概念性的:如果一个类具有const成员,则该类是const。基本上是不能分配常量对象。

只需要将其设为非常量的私有,并以不可变方式公开其值。对于用户而言,在常数方面是等效的。它允许隐式生成的函数正常工作。


说得好。我猜是在const int c之后的评论让我认为没有其他解决@eisbaw情况的方法。古老的格言是正确的:“为什么要让事情变得复杂?” - Lee Netherton
实际上,"仅仅删除'const'"并不那么容易。为什么?因为在成员字段中删除'const'将有效地强制你在构造函数参数中删除'const',进而迫使你在使用它的地方删除'const'等(我认为,这个想法应该很清楚)。虽然使用std::vector指针的解决方案可以运作,但并不是非常优雅。顺便说一下,在VS2010中,您可以将元素推送到std::vector中而不需要赋值运算符。 - Dmitrii Semikin
1
@DmitriiSemikin 从字段中删除const不会影响参数。如果您通过值进行赋值,则该值将完美地复制到非const字段中。类C {int mX; ClassName(const int x):mX(x){}}; - srm
@GManNickG 还有另一种观点。复制对象后,它变成了一个不同的对象。因此,在对象复制上下文中,成员的const性质不适用。不幸的是,现在我必须在const成员语义和赋值给对象之间做出选择。 - Oleksa

16

一个STL容器的元素必须是可复制构造和可赋值的(而你的类A不是)。你需要重载operator=

1 : §23.1 表示 存储在这些组件中的对象的类型必须满足可复制构造类型(20.1.3)的要求,并且满足可赋值类型的其他要求


编辑:

免责声明:我不确定下面的代码是否100%安全。如果它调用了UB或其他问题,请让我知道。

A& operator=(const A& assign)
{
    *const_cast<int*> (&c)= assign.c;
    return *this;
}

编辑 2

我认为上述代码片段会引起未定义行为,因为尝试去除 const 修饰变量的常量性会引发 UB


4
很抱歉,您不能在重载的operator=中对this->c进行赋值操作,因为它是一个常量。 - Prasoon Saurav
2
@Prasoon:如果*this被声明为const(例如A const obj(3); obj = A(42);),那么它就是未定义的行为。否则,这只是一个坏主意。 - Roger Pate
2
@Roger:是的,没错。§7.1.5.1/4说道:除了任何声明为mutable(7.1.1)的类成员可以被修改外,在其生命周期(3.8)内尝试修改const对象都会导致未定义的行为。但是我们这里没有任何const对象,所以虽然这是个坏主意,但应该没问题。 - Prasoon Saurav
1
实际上你的说法并不完全正确。vector 要求元素可拷贝构造和可赋值,但并不是所有容器都强制执行这个规则(list 不强制执行赋值性)。 - CashCow
1
A类中声明了cconst,所以代码总是UB,对吗? - Mark B
显示剩余7条评论

7

您缺少一个赋值运算符(或复制赋值运算符),其中之一是大三元


2

存储类型必须符合可复制和可分配的要求,这意味着需要operator=。


1

可能是赋值运算符。编译器通常会为您生成默认的赋值运算符,但由于您的类具有非平凡的复制语义,该功能已被禁用。


0

你还需要实现一个拷贝构造函数,它的样子应该是这样的:

class A {
public:
    const int c; // must not be modified!

    A(int _c)
    ...

    A(const A& copy)
    ...  

    A& operator=(const A& rhs)
    {
        int * p_writable_c = const_cast<int *>(&c);
        *p_writable_c = rhs.c;
        return *this;
    }

};

特殊的const_cast模板接受一个指针类型并将其转换回可写形式,以便在此类情况下使用。

需要注意的是,const_cast并不总是安全的,参见此处


0

在没有使用const_cast的情况下解决问题。

A& operator=(const A& right) 
{ 
    if (this == &right) return *this; 
    this->~A();
    new (this) A(right);
    return *this; 
} 

2
这不是将一种未定义行为替换为另一种吗? - TheUndeadFish
1
A const obj (42); obj = obj; - Roger Pate
@TheUndeadFish。这里的UB在哪里? - Alexey Malistov
@Alexey:派生类发生了UB;这只是实现复制赋值的一种不好的方式。 - Roger Pate
@Alexey 嗯,我可能搞错了。我以为我看到过一次讨论,表明像那样重构一个对象是未定义的行为。但现在我找不到了,所以我可能记错了。 - TheUndeadFish
显示剩余5条评论

0

我最近遇到了同样的情况,我使用了std::set,因为它添加元素的机制(insert)不需要=运算符(使用<运算符),而不像vector的机制(push_back)。

如果性能是一个问题,你可以尝试unordered_set或其他类似的东西。


0
我认为你正在使用的vector函数的STL实现需要一个赋值运算符(请参考Standard中Prasoon的引用)。然而,根据以下引用,由于你代码中的赋值运算符是隐式定义的(因为它没有显式定义),所以你的程序是不合法的,因为你的类还有一个const非静态数据成员。
C++03
“$12.8/12-“当一个其类类型的对象被赋予其类类型的值或从其类类型派生的类类型的值时,将隐式定义一个隐式声明的复制赋值运算符。如果隐式定义复制赋值运算符的类具有:
- const类型的非静态数据成员,或 - 引用类型的非静态数据成员,或 - 具有不可访问的复制赋值运算符的类类型(或其数组),或 - 具有不可访问的基类的复制赋值运算符。
则该程序是不合法的。”

0

我想指出的是,自从C++11及以后版本,问题中的原始代码编译没有任何错误!完全没有错误。然而,vec.emplace_back() 更好,因为它在内部使用"placement new",因此更高效,直接将对象复制构造到向量末尾的内存中,而不需要额外的中间复制。

cppreference状态(强调添加):

std::vector<T,Allocator>::emplace_back

将一个新元素添加到容器的末尾。该元素通过 std::allocator_traits::construct 来构造,通常使用placement-new在容器提供的位置上进行就地构造元素。

这里有一个快速演示,显示现在vec.push_back()vec.emplace_back()都能正常工作。

在这里运行:https://onlinegdb.com/BkFkja6ED

#include <cstdio>
#include <vector>

class A {
public:
    const int c; // must not be modified!

    A(int _c)
    :   c(_c)
    {
        // Nothing here
    }

    // Copy constructor 
    A(const A& copy)
    : c(copy.c)
    {
        // Nothing here
    }    
};

int main(int argc, char *argv[])
{
    A foo(1337);
    A foo2(999);

    std::vector<A> vec;
    vec.push_back(foo); // works!
    vec.emplace_back(foo2); // also works!
    
    for (size_t i = 0; i < vec.size(); i++)
    {
        printf("vec[%lu].c = %i\n", i, vec[i].c);
    }
    
    return 0;
}

输出:

vec[0].c = 1337
vec[1].c = 999

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接