停止使用make_shared进行堆分配

6

我想强制将我的对象放在栈上以实施非常严格的语义并解决一些生存期方面的问题。我已经阅读了一些关于如何做到这一点的文章,得出了将 operator new 设为私有(或删除)的结论。当直接使用 new 时,这似乎按预期工作,但 make_shared 编译通过。

#include <boost/smart_ptr.hpp>

class A
{
private:
   void *operator new( size_t );
   void operator delete( void* );
   void *operator new[]( size_t );
   void operator delete[]( void* );
};

int main()
{
//  A* a = new A;      // Correctly produces compile error
    boost::shared_ptr<A> a2 = boost::make_shared<A>();
}

直接使用 new A 会如预期地导致以下错误:

error: ‘static void* A::operator new(size_t)’ is private

我猜测make_shared能够工作是因为它使用了放置new运算符,但我找不到任何讨论如何禁止这样做的文章。我想到的最好解决方案是显式删除make_shared的模板特化。
namespace boost
{
    template<>
    shared_ptr<A> make_shared<A>() = delete;
};

显然,这非常特定于 boost::make_shared。但这是最好的方法吗?


你的代码中缺少一些 static 修饰符吗? - Ben Voigt
1
《More Effective C++》的第27条建议,如果你想要实现的目标是可能的,那么它会非常困难。Sutter的保护措施也适用于马基雅维利:任何可以使用动态分配来不正确使用您的类型的用户也可以通过语言设计以其他方式不正确地使用它。通过仅限制构造函数来解决生命周期问题——对象所在的位置不应该影响其生命周期。 - Luc Danton
@Luc 我只是想让用户在将其放在堆上并不使用它作为堆栈变量时三思而行。在堆上的语义仍然是正确的,只是在这种情况下更容易被误用一些。 - JaredC
为了让您有所了解,更加有效的C++是在1996年出版的。您想要实现的目标当然是合理和可取的。如果有一种方便的方法来做到这一点,我们都会使用它。但事实证明,语言并没有设置这样的方式,我们也没有找到一个值得的方法。 - Luc Danton
@Luc 谢谢。我已经限制了一些构建,但是然后开始陷入这个死胡同。我将从构建角度看问题,以便从那个角度重新调整问题。 - JaredC
2个回答

4
new 的放置形式非常容易处理 -- 它们只是带有额外参数的对象。例如,简单的放置形式为:
void* operator new(std::size_t, void*);

请注意,18.6.1.3禁止在全局范围内重新定义这些内容。但是,您可以重新定义(或删除/使其不可访问)它们以适应您的特定类型,这应该不会有问题。
不幸的是,make_shared使用了作用域::new(pv) T(std::forward(args)...)。正如我所提到的,您不能干涉全局放置new。因此,您无法在编译时阻止它,任何运行时陷阱都将是一个hack(检查this指针是否在栈的边界内)。

在将此内容添加到 A 后,它仍然可以正常编译。 - JaredC
@JaredC:嗯,让我查一下make_shared的标准措辞。你是在使用boost::make_shared还是std::make_shared?我看到了一个boost头文件。 - Ben Voigt
boost::make_shared。与std::make_shared没有区别。 - JaredC

2
通过仅使任何操作符不可访问,您无法强制要求类的对象始终位于堆栈上:可以在堆栈上构造的任何对象也可以作为成员嵌入到另一个对象中。即使您的原始类可能难以分配到堆中,包含类也不会。我认为这就是boost::make_shared()情况下发生的事情:内部可能会分配一些记录,其中包含其管理数据和实际被分配的对象。或者,它可能使用来自某种分配器的分配函数,这些分配函数不映射到类型的operator new(),而是使用其自己的operator new()重载。

我不确定是否可以防止堆分配(至少在将对象嵌入到另一个对象时),但任何这样做的方法都需要使构造函数不可访问(最有可能是private),并使用某种工厂函数,可能与移动相结合。另一方面,如果您可以移动对象,则具有可访问的构造函数,并且没有任何东西阻止对象被移动到堆上的对象中。

如果您特别想防止对具体类型使用std::make_shared()(或boost::make_shared(),虽然我无法引用后者的特定规则),您可以专门化std::make_shared():根据 17.6.4.2.1 [namespace.std]第1段,如果涉及用户定义类型,则允许用户专门化任何模板(除非另有规定)。因此,您可以防止将Astd::make_shared()一起使用:

class A
{
public:
    A();
    A(int);
};

namespace std
{
    template <> std::shared_ptr<A> make_shared<A>() = delete;
    template <> std::shared_ptr<A> make_shared<A, int>(int&&) = delete;
}
namespace boost
{
    template <> boost::shared_ptr<A> make_shared<A>() = delete;
    template <> boost::shared_ptr<A> make_shared<A, int>(int&&) = delete;
}

显然,如果你在类A中有多个构造函数,你可能需要添加更多的特化。... 如果你的类型恰好是一个类模板或者你的构造函数是一个模板,那么你就会很倒霉:你无法部分特化函数模板。
关于放置new的问题(这可能或可能不被make_shared()使用):放置new(和delete)的签名如下:
void* operator new(size_t, void*) noexcept;
void* operator new[](size_t, void*) noexcept;
void  operator delete(void*, void*) noexcept;
void  operator delete[](void*, void*) noexcept;

(见18.6 [support.dynamic]第1段)。我怀疑使它们不可访问并不能帮助你什么。

谢谢。我只是想在常见情况下强制执行这个规则,以防止有人意外地这样做。如果他们想要绕过它,那就随他们去吧,但至少在他们的代码中很明显他们正在做一些非标准的事情。不幸的是,make_shared 对我来说是一个非常常见的情况,这就是为什么我想要阻止它的原因。 - JaredC
不,make_shared 不是将对象嵌入类成员中。它使用作用域放置 new。 - Ben Voigt
@JaredC:如果你特别想要防止使用 make_shared(),那就变成了一个完全不同的问题:标准函数模板能否为用户定义的类型进行特化?答案是:如果特化涉及到用户定义的类型,你可以显式地从标准库中特化一个模板(17.6.4.2.1 [namespace.std] 第1段)。也许你想要 template <> shared_ptr<A> make_shared<A>() = delete;(或类似的内容)。 - Dietmar Kühl

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