Как реализовать простой контейнер с функциями размещения new и emplace?

Мне нужно реализовать контейнер для хранения некоторого количества элементов, и по какой-то причине он должен работать без выделения кучи. Другое требование состоит в том, что элементы контейнера нельзя копировать или перемещать каким-либо образом. Они должны быть встроены непосредственно в память, выделенную контейнером.

Для этого я решил использовать новое размещение и полностью делегировать управление памятью реализации контейнера (нашел полезную информацию о новом размещении на drdobbs).

Рабочий пример можно найти здесь. (Обратите внимание, что использование new uint8_t[size] и std::queue сделано только для того, чтобы сделать пример простым. Вместо этого мой реальный код имеет более сложную реализацию без кучи.)

Это прекрасно работает до сих пор, так как клиентский код должен помещать элементы в контейнер с такими вызовами, как:

executer.push(new (executer) MyRunnable("Hello", 123));

Теперь я хочу устранить необходимость повторной записи executer в этом заявлении. Я бы предпочел написать что-то вроде, например:

executer.pushNew(MyRunnable("Hello", 123));

or

executer.pushNew(MyRunnable, "Hello", 123);

возможно, предоставив соответствующий шаблон, но я не смог его написать (без макросов препроцессора, пожалуйста).

Я нашел полезную информацию о std::allocator здесь по адресу drdobbs но не знаю, как это применить к моей задаче (к тому же, статья датирована 2000 годом, поэтому не пользуйтесь возможными преимуществами C++11).

Может ли кто-нибудь помочь мне найти способ больше не давать executer дважды?

Редактировать: после успешного утверждения ответа Jarod42 я обновил свой рабочий код примера здесь.

И для истории, здесь исходный пример кода моего первоначального вопроса:

#include <iostream>
#include <queue>


class Runnable {
    // Runnable should be uncopyable and also unmovable
    Runnable(const Runnable&) = delete;
    Runnable& operator = (const Runnable&) = delete;    
    Runnable(const Runnable&&) = delete;
    Runnable& operator = (const Runnable&&) = delete;    
public:
    explicit Runnable() {}
    virtual ~Runnable() {}
    virtual void run() = 0;
};


class MyRunnable: public Runnable {
public:
    explicit MyRunnable(const char* name, int num): name(name), num(num) {}
    virtual void run() override {
        std::cout << name << " " << num << std::endl;
    }
private:
    const char* name;
    int num;
};


class Executer {
    // Executer should be uncopyable and also unmovable
    Executer(const Executer&) = delete;
    Executer& operator = (const Executer&) = delete;    
    Executer(const Executer&&) = delete;
    Executer& operator = (const Executer&&) = delete;    
public:
    explicit Executer() {    
    }

    void* allocateEntry(size_t size) {
        // this heap allocation is just to keep this example simple
        // my real implementation uses it's own memory management instead (blockpool)
        return new uint8_t[size];
    }

    void push(Runnable* entry) {
        queue.push(entry);
    }

    template <typename R> // this don't works
    void pushNew(R) {
        push(new (*this) R);
    }

    inline friend void* operator new(size_t n, Executer& executer) {
        return executer.allocateEntry(n);
    }

    void execute() {
        while (queue.size() > 0) {
            Runnable* entry = queue.front();
            queue.pop();
            entry->run();
            // Now doing "placement delete"
            entry->~Runnable();
            uint8_t* p = reinterpret_cast<uint8_t*>(entry);
            delete[] p;
        }

    }

private:
    // this use of std::queue is just to keep this example simple
    // my real implementation uses it's own heap-less queue instead
    std::queue<Runnable*> queue {};
};


int main() {
    Executer executer;
    executer.push(new (executer) MyRunnable("First", 1));
    executer.push(new (executer) MyRunnable("Second", 2));
    executer.push(new (executer) MyRunnable("Third", 3));

    // but want to use it more like one this 
    //executer.pushNew(MyRunnable("Fifth", 5));  // how to implement it?
    //executer.pushNew(MyRunnable, "Sixth", 6);  // or maybe for this usage?

    executer.execute();
}

person Joe    schedule 03.10.2015    source источник
comment
Известно ли количество элементов во время компиляции? Не могли бы вы использовать std::array?   -  person Vaughn Cato    schedule 03.10.2015
comment
Нет, std::array использовать нельзя. Кроме того, способ управления памятью контейнером должен быть полностью закрытым.   -  person Joe    schedule 03.10.2015
comment
Вы смотрели на реализацию std::vectors emplace назад? Похоже, это именно то, что вам нужно (игнорируя проблему haep vs stack, которая является отдельной проблемой)   -  person MikeMB    schedule 03.10.2015
comment
Если подумать, вы могли бы даже создать оболочку вокруг std::vector с помощью распределителя стека.   -  person MikeMB    schedule 03.10.2015
comment
Вместо того, чтобы передавать MyRunnable в push(), не могли бы вы вместо этого передать аргументы (name и num)? Тогда у вас может быть new внутри push().   -  person Vaughn Cato    schedule 03.10.2015
comment
Я думаю, что делал то же самое раньше (в функции AddNew): at=4.x-dev&fileviewer=file-view-default" rel="nofollow noreferrer">bitbucket.org/darkgazeorg/gorgon-game-engine/src/   -  person Cem Kalyoncu    schedule 03.10.2015
comment
Аргументы MyRunnable ctor неизвестны контейнеру, и могут быть FooRunnable и BarRunnable даже с разными параметрами. Звучит как необходимость использовать шаблон с переменным числом аргументов…   -  person Joe    schedule 03.10.2015
comment
этот вопрос, возможно, является дубликатом: stackoverflow. com/questions/28628484/ вы запрашиваете функциональность emplace, которая вставлена ​​по дизайну (по крайней мере, в C++ 11 я начинаю забывать о старом C++ xD)   -  person CoffeDeveloper    schedule 04.10.2015
comment
Вы правы, мой вопрос как раз о функциональности »emplace« (и теперь я переименую функцию-член в своем коде. Для этого она может дублироваться. Но я думаю, что ответы Jarod42 и Barry действительно красивы и Ясно Наконец-то я добавил »emplace« к теме вопросов.   -  person Joe    schedule 05.10.2015


Ответы (2)


С участием:

template <typename R, typename... Ts>
void pushNew(Ts&&... args) {
    push(new (*this) R(std::forward<Ts>(args)...));
}

Ты можешь написать:

executor.PushNew<MyRunnable>("Hello", 123);

вместо

executer.push(new (executer) MyRunnable("Hello", 123));
person Jarod42    schedule 03.10.2015
comment
Хотя это принятый ответ, ответ Барри намного лучше. - person Martin Bonner supports Monica; 07.08.2018

В этом есть две ошибки:

template <typename R> // this don't works
void pushNew(R) {
    push(new (*this) R);
}

На первый отвечает Jarod42, что вы хотите сделать:

template <typename R, typename... Ts>
void pushNew(Ts&&... args) {
    push(new (*this) R(std::forward<Ts>(args)...));
}

но что еще более важно... new (*this) R действительно странный. Похоже, вы строите R над собой! Но это не так, вы просто используете этот синтаксис для вызова своего распределителя. Это ужасно нарушает принцип наименьшего удивления. Мне потребовалось довольно много времени, чтобы понять, что происходит.

Что вам нужно, так это просто использовать свой распределитель напрямую:

template <typename R, typename... Ts>
void pushNew(Ts&&... args) {
    void* slot = allocateEntry(sizeof(R));
    push(new (slot) R(std::forward<Ts>(args)...));
}

Это намного легче понять.

person Barry    schedule 03.10.2015
comment
следует принять. Случайные навигаторы имейте в виду, что это ответ, который я предпочитаю! :) - person CoffeDeveloper; 04.10.2015