31

我有一个 C++ 类,我只希望它在堆栈上实例化。我正在使用 api 访问以另一种(解释)语言开发的内容,该语言带有自己的垃圾收集。该语言中的机制足以将它找到的任何引用的内容单独留在堆栈上,并且由于该本地类包含这样的引用,因此对于正确的行为至关重要的是,它所执行的本地 C++ 类的用户永远不要尝试在其他任何地方分配它的实例。

注意,我不仅想禁止用 new 分配我的类的实例(如果这就是我需要做的,我可以重载类的new运算符并将其设为私有,或者从 C++11 开始显式删除它),但也不允许该类的任何静态或可能的全局实例。安全地实例化这个类的唯一有效方法应该是在堆栈上,我想以某种方式保证这一点。据我所知,将其设为new私有或删除它也不会阻止将另一个类声明为我的类作为成员变量,并在堆上分配该类的实例。

我现在的管理方式是将“本地”一词作为类名称的一部分,作为对用户的友好提醒,该实例仅用于堆栈,但当然,这不是t 实际上是由编译器或任何其他机制强制执行的,我更喜欢一个更具可执行性的解决方案。

理想情况下,我想在编译时确保这一点,如果使用不当,编译会失败。如果这根本不可能,那么在构造实例时在运行时抛出异常仍然是可接受的回退。在 C++11 或 C++14 中工作的解决方案很好。

请注意,这个问题绝对这个问题不同,它只是想防止分配new

4

3 回答 3

40

免责声明:据我所知,“堆栈”不是 c++ 标准的一部分,我们有 ASDV(自动存储持续时间变量)。ABI 可能会定义堆栈。请注意,有时这些是在寄存器中传递的,我认为在您的情况下是可以的。

定义一个 CPS(continuation-passing style)工厂方法:

class A {
public:
   template<typename F, typename... Args>
   static auto cps_make(F f, Args&&... args) {
      return f(A(std::forward<Args>(args)...));
   }
private:
   A(/* ... */) {}
   A(const A&) = delete;
   A(A&&) = delete;
};

用法:传递一个带 A 的 lambda 和 AEg 的 ctor 参数

return A::cps_make([&](A a) {
   /* do something with a */
   return true;
});

函数参数始终是内部的 ASDV。

代码是如何工作的:cps_make 接受一个函子(通常是一个 lambda),它接受一个给定类型的实例;和可选的ctor参数。它创建实例(通过将任何可选参数转发给 ctor),调用函子并返回函子返回的内容。由于函子可以是 C++11 中的 lambda,因此它不会破坏正常的代码流。

CPS 的美妙之处在于,您只需在 C++14 中使用自动 lambda 即可拥有静态多态性:您的 cps_make() 可以创建几乎任何您想要的东西(层次结构、变体、任何等)。然后为封闭的层次结构节省虚拟开销。你甚至可以有一个用于正常流程的 lambda,如果 ctor 会失败,则可以有一个;当异常不可行时,这很方便。

缺点是,目前你不能直接在 lambda 内部使用外部作用域的控制流语句。/* 提示:我们正在努力。*/

于 2017-04-19T22:21:08.260 回答
6

好的,这是我的看法:

struct stack_marker
{
    thread_local static uint8_t* marker;
    uint8_t stk;

    stack_marker()
    {
        if (marker != nullptr)
        {
            throw std::runtime_error("second twice marker! don't do it");
        }
        marker = &stk;
    }
};

thread_local uint8_t* stack_marker::marker = nullptr;

void sort3(uint8_t* (&a)[3]); //sorts 3 pointers, see gist

class only_on_stack
{
    uint8_t place;
public:
    NO_INLINE only_on_stack(int x)
    {
        uint8_t a;

        if (!stack_marker::marker)
        {
            // not initialized yet, either forgot to put stack_marker in main
            // or we are running before main, which is static storage

            //throw std::runtime_error("only on stack object created in non-stack");
            std::cout << x << ": I'm NOT on stack\n";
            return;
        }

        uint8_t* ptrs[] = {
            stack_marker::marker,
            &place,
            &a
        };

        sort3(ptrs);

        if (ptrs[1] == &place) // place must be in the middle
        {
            std::cout << x << ": I'm on stack\n";
        }
        else
        {
            //throw std::runtime_error("only_on_stack object created in non-stack");
            std::cout << x << ": I'm NOT on stack\n";
        }
    }
};

only_on_stack static_storage(1);
thread_local only_on_stack tl_storage(4);

int NO_INLINE stuff()
{
    only_on_stack oos(2);
}

int main()
{
    stack_marker mrk;
    stuff();
    auto test = new only_on_stack(3);
    tl_storage; // access thread local to construct, or gcc omits it
}

诚然,我的解决方案并不是所有解决方案中最干净的,但它允许您继续使用常规的本地对象语法。

基本上,诀窍是将 2 个额外的对象放在堆栈上,而不是我们的对象:一个在线程的开头,一个在构造函数。因此,其中一个对象是在我们的对象之后在堆栈上创建的,其中一个是在之前创建的。有了这些信息,我们就可以检查这 3 个对象的地址顺序。如果对象真的在堆栈上,它的地址应该在中间。

但是,C++ 没有定义函数范围内对象的地址顺序,因此执行以下操作:

int main()
{
    int a;
    int b;
    int c;
}

保证&b&aand中间&c

为了解决这个问题,我们可以保留a在 main 函数中,bc在不同的force 非内联函数中移动:

void NO_INLINE foo()
{
    int b;
    int c;
}

int main()
{
    int a;
    foo();
}

在这种情况下,由于编译器无法知道fooin main&a>或<的局部变量,&b所以。通过将相同的东西移动到另一个不可内联的函数,我们可以保证 &b 在and的中间。&c&a&b&cc&a&c

在我的实现中,stufffunction 是foo函数,我们c进入的函数是only_on_stack.

实际工作实现在这里:https ://gist.github.com/FatihBAKIR/dd125cf4f06cbf13bb4434f79e7f1d43

无论堆栈向下还是向上增长,并且无论目标文件类型如何,并且希望是 ABI,它都应该工作,只要编译器不会以某种方式重新排序非内联函数的局部变量。

-O3在 linux 上的 g++-6 和 mac os x 上的最新 clang 上进行了测试。它应该可以在 MSVC 上运行,希望有人可以测试它。

两者的输出是:

1: I'm NOT on stack
2: I'm on stack
3: I'm NOT on stack
4: I'm NOT on stack

用法基本上是,您将一个stack_marker对象放在每个线程(main包括)的开头并调用另一个不可内联的函数并将其用作您的实际入口点。

于 2017-04-19T23:15:19.780 回答
5

一种可能性是只允许临时变量(延长寿命),例如:

class A
{
private:
    A() = default;
    A(const A&) = delete;
    A(A&&) = delete;
    A& operator =(const A&) = delete;
    A& operator =(A&&) = delete;

public:
    static A Make() { return {}; } 
};

auto&& g = A::Make(); // possible :/

int main() {
    auto&& a = A::Make(); // possible

#if 0
    new A(); // error

    struct InnerA
    {
        A a; // error
    };
#endif
}

在保证复制省略的情况下,它在 C++17 中将不再有效。

于 2017-04-19T23:27:18.647 回答