C++的默认拷贝构造函数本质上是不安全的吗?迭代器根本上也不安全吗?

Meh*_*dad 36 c++ iterator copy-constructor default-copy-constructor

我曾经认为,当遵循最佳实践时,C++的对象模型非常强大.
就在几分钟前,我意识到我以前没有过.

考虑以下代码:

class Foo
{
    std::set<size_t> set;
    std::vector<std::set<size_t>::iterator> vector;
    // ...
    // (assume every method ensures p always points to a valid element of s)
};
Run Code Online (Sandbox Code Playgroud)

我写了这样的代码.直到今天,我还没有看到它的问题.

但是,考虑到它更多,我意识到这个类非常破碎:
它的copy-constructor和copy-assignment 复制里面的迭代器vector,这意味着它们仍然会指向旧的 set!毕竟新的不是真正的副本!

换句话说,我必须手动实现copy-constructor,即使这个类没有管理任何资源(没有RAII)!

这令我惊讶.我以前从未遇到过这个问题,我也不知道有什么优雅的方法来解决它.关于它的思考多一点,在我看来,那拷贝构造是默认不安全的 -事实上,在我看来那类应该不会是在默认情况下拷贝,但因为他们的实例变量之间的任何一种耦合的风险再现默认副本- 构造函数无效.

迭代器是否从根本上说不安全?或者,默认情况下类是否真的不可复制?

我在下面想到的解决方案都是不可取的,因为它们不会让我利用自动生成的复制构造函数:

  1. 为我编写的每个重要类手动实现一个拷贝构造函数.这不仅容易出错,而且对于编写复杂的类也很痛苦.
  2. 永远不要将迭代器存储为成员变量.这似乎是严重的限制.
  3. 默认情况下,对我写的所有类禁用复制,除非我能明确证明它们是正确的.这似乎完全违背了C++的设计,这对于大多数类型来说都具有值语义,因此是可复制的.

这是一个众所周知的问题,如果是这样,它是否有优雅/惯用的解决方案?

Yak*_*ont 21

C++ copy/move ctor/assign对于常规值类型是安全的.常规值类型的行为类似于整数或其他"常规"值.

它们对指针语义类型也是安全的,只要操作不改变指针"应该"指向的内容即可.指向"你自己"或其他成员的东西,就是它失败的一个例子.

它们对于引用语义类型有些安全,但在同一类中混合指针/引用/值语义在实践中往往是不安全/错误/危险的.

零规则是您创建的行类似于常规值类型,或者不需要在复制/移动时重新设置的指针语义类型.然后你不必写复制/移动ctors.

迭代器遵循指针语义.

围绕这个的惯用/优雅是将迭代器容器与指向容器紧密耦合,并在那里阻塞或写入副本.一旦包含指向另一个的指针,它们就不是真正独立的东西.

  • @Mehrdad:对,它没有它们的原因是因为几个标准容器没有有效的方法来实现它们.`vector`和`deque` do,但对于那些类型,你可以使用`size_t`索引作为句柄,所以没有任何意义使它成为正式的抽象.`map`和`unordered_map`可以使用键类型作为句柄.对于`set`,句柄必须是值本身,所以在你的例子中存储一个`vector <size_t>`. (3认同)

Chr*_*odd 18

是的,这是一个众所周知的"问题" - 每当你在一个对象中存储指针时,你可能需要某种自定义复制构造函数和赋值运算符来确保指针都是有效的并指向预期的东西.

由于迭代器只是集合元素指针的抽象,因此它们具有相同的问题.

  • 这并没有回答"如果是这样,它有一个优雅/惯用的解决方案吗?" 部分问题.(我认为答案只是"不":如果使用默认的复制构造函数,编写类的人需要验证默认的复制构造函数是否合适.) (6认同)
  • @hvd:"优雅/惯用"解决方案是写一个拷贝ctor/copy assign op - 这就是使用定义的拷贝ctors/assign ops存在*for* (2认同)

Mat*_* M. 14

这是一个众所周知的问题吗?

嗯,众所周知,但我不会说众所周知.兄弟姐妹指针不经常发生,我在野外看到的大多数实现都以与你的完全相同的方式被破坏.

我认为这个问题很少发生在大多数人的注意之中; 有趣的是,由于我现在遵循比C++更多的Rust,因为类型系统的严格性(即编译器拒绝这些程序,提示问题),它经常出现在那里.

它有优雅/惯用的解决方案吗?

有许多类型的兄弟指针情况,所以它真的取决于,但我知道两个通用的解决方案:

  • 按键
  • 共享元素

让我们按顺序查看它们.

指向一个类成员,或指向一个可索引的容器,然后可以使用偏移量而不是迭代器.效率略低(并且可能需要查找),但这是一个相当简单的策略.我已经看到它在共享内存情况下使用效果很好(因为共享内存区域可能映射到不同的地址,因此使用指针是禁忌的).

另一种解决方案由Boost.MultiIndex使用,并包含在另一种内存布局中.它源于侵入式容器的原理:不是将元素放入容器(在内存中移动),而是一个侵入式容器使用已经在元素内部的钩子将它连接到正确的位置.从那里开始,很容易使用不同的钩子将单个元件连接到多个容器中,对吗?

好吧,Boost.MultiIndex进一步推动了两步:

  1. 它使用传统的容器接口(即移动对象),但移动对象的节点是具有多个钩子的元素
  2. 它在单个实体中使用各种钩子/容器

您可以查看各种示例,特别是示例5:Sequenced Indices看起来很像您自己的代码.


Rem*_*eau 9

这是一个众所周知的问题

是.每当你有一个包含指针的类或像迭代器这样的指针式数据时,你必须实现自己的copy-constructor和assignment-operator来确保新对象具有有效的指针/迭代器.

如果是这样,它有一个优雅/惯用的解决方案吗?

也许不像你想的那么优雅,并且可能不是最好的性能(但是,副本有时候不是,这就是为什么C++ 11增加了移动语义),但也许这样的东西对你有用(假设std::vector包含到std::set同一父对象的迭代器):

class Foo
{
private:
    std::set<size_t> s;
    std::vector<std::set<size_t>::iterator> v;

    struct findAndPushIterator
    {
        Foo &foo;
        findAndPushIterator(Foo &f) : foo(f) {}

        void operator()(const std::set<size_t>::iterator &iter)
        {
            std::set<size_t>::iterator found = foo.s.find(*iter);
            if (found != foo.s.end())
                foo.v.push_back(found);
        }
    };

public:
    Foo() {}

    Foo(const Foo &src)
    {
        *this = src;
    }

    Foo& operator=(const Foo &rhs)
    {
        v.clear();
        s = rhs.s;

        v.reserve(rhs.v.size());
        std::for_each(rhs.v.begin(), rhs.v.end(), findAndPushIterator(*this));

        return *this;
    }

    //...
};
Run Code Online (Sandbox Code Playgroud)

或者,如果使用C++ 11:

class Foo
{
private:
    std::set<size_t> s;
    std::vector<std::set<size_t>::iterator> v;

public:
    Foo() {}

    Foo(const Foo &src)
    {
        *this = src;
    }

    Foo& operator=(const Foo &rhs)
    {
        v.clear();
        s = rhs.s;

        v.reserve(rhs.v.size());
        std::for_each(rhs.v.begin(), rhs.v.end(),
            [this](const std::set<size_t>::iterator &iter)
            {
                std::set<size_t>::iterator found = s.find(*iter);
                if (found != s.end())
                   v.push_back(found);
            } 
        );

        return *this;
    }

    //...
};
Run Code Online (Sandbox Code Playgroud)


Lig*_*ica 7

是的,当然这是一个众所周知的问题.

如果您的类存储了指针,那么作为一名经验丰富的开发人员,您会直观地知道默认的复制行为可能不足以满足该类的要求.

您的类存储迭代器,并且由于它们也是存储在其他位置的数据的"句柄",因此适用相同的逻辑.

这几乎不是"惊人的".


tah*_*ith 5

Foo不管理任何资源的断言是错误的.

除了复制构造函数,如果set删除了一个元素,那么必须有Foo管理的代码,vector以便删除相应的迭代器.

我认为惯用的解决方案是只使用一个容器a vector<size_t>,并在插入之前检查元素的计数是否为零.然后复制和移动默认值很好.