Есть ли способ обеспечить, чтобы экземпляры были только в стеке?

У меня есть класс C++, для которого я только хочу, чтобы он был создан в стеке. Я использую API для доступа к контенту, который был разработан на другом (интерпретированном) языке, который поставляется со своей собственной сборкой мусора. Механизмы в этом языке знают достаточно, чтобы оставить любой контент, на который он находит ссылки, только в стеке, и поскольку этот собственный класс содержит такую ​​ссылку, жизненно важно, чтобы для правильного поведения пользователь собственного класса C++ это делал никогда не пытайтесь выделить экземпляр этого где-либо еще.

Обратите внимание, я не только хочу запретить экземпляру моего класса выделяться новым (если бы это было все, что мне нужно было сделать, я мог бы перегружать класс new оператор и сделать его закрытым, или явно удалить его после C++11), но также запретить любые статические или возможные глобальные экземпляры класса. Единственный надежный способ создания экземпляра этого класса должен быть в стеке, и я хотел бы как-то это гарантировать. Насколько я знаю, делая new закрытое или удаленное также не препятствует тому, чтобы другой класс был объявлен с моим классом как переменная-член, и экземпляр этого был размещен в куче.

Я управляю этим прямо сейчас, чтобы слово "Local" было частью имени класса в качестве дружеского напоминания пользователю, что экземпляр предназначен только для использования в стеке, но, конечно, это не так. Это на самом деле обеспечивается компилятором или любым другим механизмом, и я бы предпочел решение, которое является более принудительным.

В идеале я хочу убедиться в этом во время компиляции и потерпеть неудачу при неправильном использовании. Если это просто невозможно, создание исключения во время выполнения при создании экземпляра все еще является приемлемым резервным вариантом. Решения, которые работают в C++ 11 или C++14, хороши.

Обратите внимание, что этот вопрос определенно НЕ такой, как этот, который хотел только предотвратить размещение new

3 ответа

Отказ от ответственности: "стек" не является частью стандарта C++, насколько я знаю, там у нас есть ASDV (переменные продолжительности автоматического хранения). ABI может определить стек. Обратите внимание, что иногда они передаются в регистрах, что, я считаю, нормально в вашем случае.

Определите метод фабрики CPS (стиль продолжения):

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 и параметры ctor AEg

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

Аргументы функции всегда внутри ASDV.

Как работает код: cps_make использует функтор (обычно лямбда), который принимает экземпляр данного типа; и необязательные параметры ctor. Он создает экземпляр (перенаправляя любые необязательные параметры в ctor), вызывает функтор и возвращает то, что возвращает функтор. Поскольку функтор может быть лямбда-выражением в C++11, он не нарушает нормальный поток кода.

Прелесть CPS в том, что вы можете иметь статический полиморфизм, просто используя автоматическую лямбду в C++14: ваш cps_make() может создавать практически все, что вы пожелаете (иерархия, вариант, любой и т. Д.). Затем вы сохраняете виртуальные издержки для закрытых иерархий. Вы можете даже иметь лямбду для нормального потока и один, если ctor не сможет работать; это удобно, когда исключения не разрешены.

Недостатком является то, что в настоящее время вы не можете напрямую использовать операторы потока управления внешней области видимости внутри лямбды. /* Подсказка: мы работаем над этим. */

Хорошо, вот мое мнение:

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находится в середине&a а также &c,

Чтобы обойти это, мы могли бы сохранитьaв основной функции и двигатьсяbа также c в другой силе не встроенная функция:

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

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

В этом случае, поскольку компилятор не может знать локальные переменныеfooвmain, &a > &b, &c или же &a< &b, &c, Применяя то же самое кcпереместив его в другую не встроенную функцию, мы можем гарантировать, что &b находится в середине &aа также &c,

В моей реализации,stuffфункция являетсяfooфункция и функция, которую мы перемещаем c в это конструктор only_on_stack,

Фактическая рабочая реализация здесь: https://gist.github.com/FatihBAKIR/dd125cf4f06cbf13bb4434f79e7f1d43

Он должен работать независимо от того, растет ли стек вниз или вверх, независимо от типа объектного файла и, возможно, от ABI, если компилятор не каким-либо образом переупорядочивает локальные переменные не встроенных функций.

Это было проверено с-O3на g++-6 на linux и последний clang на mac os x. Ондолжен работать на 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включите) и вызовите другую не встроенную функцию и используйте ее в качестве фактической точки входа.

Возможность состоит в том, чтобы разрешить только временные переменные (с увеличенным временем жизни), что-то вроде:

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 с гарантированным разрешением копирования.

Другие вопросы по тегам