std::make_shared 导致未定义的行为,但新作品

std::make_shared leads to undefined behavior, but new works

考虑以下示例 class:

class Foo {

public:
    void* const arr_;

    Foo() = delete;

    Foo(const size_t size, bool high_precision) :
        arr_(Initialize(size, high_precision)) {};

    template <typename T>
    T* GetDataPointer() {
        return (T* const)arr_;
    }
private:
    static void* Initialize(const size_t size, bool high_prec) {
        if (high_prec) {
            return new double[size];
        }
        else {
            return new float[size];
        }
    }

};

当我使用 std::make_shared 创建指向 Foo 对象的共享指针时,我经常发现我初始化的数组数据显示未定义 behavior/becomes 已损坏。例如:

std::shared_ptr<Foo> sp_foo = std::make_shared<Foo>(Foo(3,false));
    auto foo_data = sp_foo->GetDataPointer<float>();
    foo_data[0] = 1.1;
    foo_data[1] = 2.2;
    foo_data[2] = 3.3;
    std::cout << "First Value: " << 
        sp_foo->GetDataPointer<float>()[0]; // Sometimes this is 1.1, sometimes it is meaningless.

然而,当我使用 new 初始化共享指针时,这个问题似乎消失了。

std::shared_ptr<Foo> sp_foo (new Foo(3,false));
    auto foo_data = sp_foo->GetDataPointer<float>();
    foo_data[0] = 1.1;
    foo_data[1] = 2.2;
    foo_data[2] = 3.3;
    std::cout << "First Value: " << 
        sp_foo->GetDataPointer<float>()[0]; // Correctly prints 1.1

对正在发生的事情有什么想法吗?小旁注:我被迫不对 Foo class 进行模板化,并且确实有一个 void* const 到板载数据数组。

您对 make_shared 的调用正在使用您尚未定义的 Foo class 的复制构造函数。因此,将使用默认 (compiler-generated) 副本,并调用析构函数删除临时文件。由于您的 class 没有正确实现 Rule of Three,这(可能)导致未定义的行为。

正在使用复制构造函数,因为您对 std::make_shared 的调用中的参数列表是 Foo(3, false) – 一个 Foo 对象,因此调用只适合 this cppreference page 上列出的第一个重载。 (请注意,没有什么类似于 std::make_shared<T>(const T& src) 重载。)从该页面,我们看到:

  1. Constructs an object of type T and wraps it in a std::shared_ptr using args as the parameter list for the constructor of T.

因此,使用 Foo(3, false) 的“args”,make_shared 实际上会调用以下构造函数来包装对象:

Foo(Foo(3, false))

为了正确的行为,只需将 3false 作为参数传递给 make_shared:

std::shared_ptr<Foo> sp_foo = std::make_shared<Foo>(3, false);

您可以 证明 原始代码中的错误,方法是向记录一些输出的 Foo class 添加析构函数,例如:~Foo() { std::cout << "Destroying...\n"; }.您将在执行对 make_shared.

的调用后看到该输出

您可以通过删除 Foo 复制构造函数 防止 这种意外 error/oversight:Foo(const Foo& f) = delete;。这将生成如下所示的编译器错误:

error : call to deleted constructor of 'Foo'


但是,在第二种情况下,您使用 std::shared_ptr constructor(链接页面上显示的第三种形式)。这没有问题,因为构造函数 'just' 将给定的指针包装到托管对象中,不需要复制或销毁。