Частный конструктор запрещает использование emplace[_back](), чтобы избежать перемещения

Рассмотрим следующий код:

#include <vector>

class A
{
public:    
    A(A&&);  // somewhat expensive

    static std::vector<A> make_As()
    {
        std::vector<A> result;
        result.push_back(A(3));
        result.push_back(A(4));
        return result;
    }

private:
    A(int);  // private constructor
};

Поскольку конструктор перемещения A несколько дорог (по какой-то причине), я бы хотел избежать его вызова и вместо этого использовать emplace_back():

#include <vector>

class A
{
public:    
    A(A&&);  // somewhat expensive

    static std::vector<A> make_As()
    {
        std::vector<A> result;
        result.emplace_back(3);
        result.emplace_back(4);
        return result;
    }

private:
    A(int);  // private constructor
};

К сожалению, с emplace_back() фактический вызов конструктора выполняется чем-то в стандартной библиотеке, которая недостаточно привилегирована, чтобы иметь возможность вызывать частный конструктор A.

Я понимаю, что с этим, вероятно, мало что можно сделать, но, тем не менее, я чувствую, что, поскольку вызовы emplace_back() происходят внутри члена A, они должны вызывать закрытый конструктор.

Есть ли обходные пути для этого?

Единственное, что я могу придумать, это добавить декларацию друга к A, но точный класс, который должен быть другом A (то есть класс, который фактически пытается вызвать конструктор), зависит от реализации (например, для GCC это __gnu_cxx::new_allocator<A>). EDIT: только что понял, что такое объявление друга позволит любому создавать emplace_back() A с помощью частного конструктора в контейнер A, так что на самом деле это ничего не решит, я может также сделать конструктор общедоступным в этот момент...

ОБНОВЛЕНИЕ: я должен добавить, что дороговизна конструктора перемещения A — не единственная причина избегать его вызова. Возможно, A вообще нельзя перемещать (или копировать). Конечно, это не сработает с vector (поскольку emplace_back() может потребоваться перераспределить вектор), но сработает с deque, который также имеет аналогичный метод emplace_back(), но не должен ничего перераспределять.


person HighCommander4    schedule 11.07.2012    source источник
comment
Побочный комментарий, но конструкторы перемещения должны быть самой быстрой формой построения... звучит как проблема дизайна.   -  person GManNickG    schedule 11.07.2012
comment
Побочный комментарий некорректен. Это (никогда не медленнее и, за исключением других оптимизаций, как правило) быстрее построить объект, чем построить, а затем переместить объект, просто потому, что он выполняет меньше работы. emplace_back создает объект на месте.   -  person Nevin    schedule 11.07.2012
comment
@GManNickG: использование push_back повлечет за собой стоимость частного конструктора + стоимость конструктора перемещения. Использование emplace_back повлечет за собой только стоимость частного конструктора, поэтому это будет строго быстрее.   -  person HighCommander4    schedule 11.07.2012
comment
@HighCommander4: Да, но я говорю следующее: поскольку конструктор перемещения A несколько дорог (по какой-то причине), вероятно, это не должно вызывать беспокойства.   -  person GManNickG    schedule 11.07.2012
comment
@GManNickG: Кроме того, есть больше причин избегать вызова конструктора перемещения, чем избегать его затрат. Например, A может вообще не быть конструируемым для перемещения (предположим, я заменю vector на deque - это был бы правдоподобный сценарий). Я отредактирую ответ, чтобы упомянуть об этом.   -  person HighCommander4    schedule 11.07.2012
comment
@GManNickG: Наконец, есть причины, по которым конструктор перемещения может быть дорогим. Например, рассмотрим класс, который объединяет большой элемент std::array.   -  person HighCommander4    schedule 11.07.2012
comment
@HighCommander4: Я не говорю, что это не имеет значения, я говорю, что в вашем случае это вероятно не имеет значения. Вы собираетесь вернуть этот вектор, и каждый из них все равно будет перемещен, поэтому, если вы заботитесь о производительности (основа вашего поста), вы можете улучшить скорость своего перемещения.   -  person GManNickG    schedule 11.07.2012
comment
@GManNickG: Возврат вектора по значению не вызывает вызов конструктора перемещения каждого из элементов. Будет вызван конструктор перемещения вектора, который просто скопирует внутренний элемент указателя/размера вектора и вообще не коснется элементов.   -  person HighCommander4    schedule 11.07.2012
comment
@HighCommander4: Ну да, такие комментарии я делаю, когда смертельно устал. Продолжать.   -  person GManNickG    schedule 11.07.2012


Ответы (3)


Одним из возможных обходных путей (или кладжа) может быть использование вспомогательного класса для хранения параметров частного ctor A (назовем этот класс EmplaceHelper). У EmplaceHelper также должен быть частный ctor, и он должен быть во взаимной дружбе с A. Теперь все, что вам нужно, это общедоступный ctor в A, который берет этот EmplaceHelper (вероятно, через const-ref) и использует его с emplace_back(EmplaceHelper(...)).

Поскольку EmplaceHelper может быть создан только A, ваш публичный ctor по-прежнему остается приватным.

Можно даже обобщить эту идею с помощью шаблонного EmplaceHelper (возможно, используя std::tuple для хранения параметров ctor).

Редактировать: на самом деле, кажется, я слишком усложнил это, поскольку комментарий ниже от GManNickG дал мне более простую идею: добавить частный вспомогательный класс (private_ctor_t в примере), который является просто пустым классом, но поскольку он является закрытым он доступен только A. Измените конструктор A, чтобы включить этот закрытый класс в качестве первого (или последнего) параметра (и сделать его общедоступным). В результате только A сможет построить себя, как если бы у него был закрытый конструктор, но теперь эту конструкцию можно было легко делегировать.

Так:

#include <vector>
class A 
{ 
private:
    struct private_ctor_t {};

public:     
    A(private_ctor_t, int x) : A(x) // C++11 only, delegating constructor
    {}

    A(A&&) { /* somewhat expensive */ }

    static std::vector<A> make_As() 
    { 
        std::vector<A> result; 
        result.emplace_back(private_ctor_t{}, 3); 
        result.emplace_back(private_ctor_t{}, 4); 
        return result; 
    } 

private: 
    A(int) { /* private constructor */ }
}; 

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

person mitchnull    schedule 11.07.2012
comment
спасибо GManNickG за полировку :) Я сформулировал свой ответ в метро по пути на работу, тогда не смог прошить код примера ;) - person mitchnull; 11.07.2012

По стандарту C++11 все стандартные контейнеры должны использовать метод allocator::construct для построения на месте. Таким образом, вы можете просто сделать std::allocator другом A.

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

Если такое делегирование происходит или по какой-либо другой причине, вы можете предоставить свой собственный распределитель, который перенаправляет все вызовы на std::allocator, кроме construct. Я не предлагаю последнее, так как многие стандартные реализации контейнеров имеют специальный код для работы с std::allocator, что позволяет им занимать меньше места.

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

Тогда вам придется решить, что для вас важнее: строительство на месте или скрытие рядовых. По самой своей природе построение на месте означает, что кто-то не в вашем коде выполняет построение. Следовательно, нет другого пути: какой-то внешний код должен называться другом или конструктор должен быть общедоступным. Короче говоря, конструктор должен быть общедоступным доступным для всех, кому делегирована конструкция.

person Nicol Bolas    schedule 11.07.2012
comment
Я согласен с мнением, что метод, выполняющий построение, должен быть указан, учитывая нетранзитивную дружбу, которую мы имеем в C++. - person Matthieu M.; 11.07.2012
comment
Мой ответ может пролить свет на эту проблему. Это не будет работать со стандартной двухсторонней очередью со стандартным распределителем (необходимо создать подкласс), но доказывает, что это можно сделать с помощью пользовательского распределителя (но мне также пришлось переопределить rebind). Принятый здесь ответ лучше соответствует требованиям автора, поэтому я не буду размещать здесь свое решение, а просто ссылку. - person firda; 01.09.2014

Давайте немного упростим. Следующее не удается скомпилировать, поскольку V не имеет доступа к частному конструктору A.

struct V
{
    E(int i)
    {
        // ...
        auto a = A(i);
        // ...
    }
};

Возвращаясь к вашему коду, V — это просто упрощение вектора, а V::E — просто упрощение того, что делает emplace_back. vector не имеет доступа к частному конструктору A, и vector::emplace_back должен вызвать его.

person Nevin    schedule 11.07.2012
comment
Я понимаю механику, почему это не работает. Я просто ищу обходной путь. - person HighCommander4; 11.07.2012
comment
@HighCommander4: К сожалению, хорошего обходного пути нет, поскольку C++ friend довольно специфичен. Вы можете сделать его общедоступным, как вы заметили, но если вы хотите, чтобы это было немного более явным, например: «Эй, я использую эту функцию, которую мы хотели сделать частной, но не смогли!», вы можете сделать такой трюк : struct foo { struct using_private_constructor {}; foo(int, const using_private_constructor&); };, теперь место вызова выглядит так: foo f(5, foo::using_private_constructor()) или result.emplace_back(3, foo::using_private_constructor()). Может быть, вы можете оправдать просто сделать это нормальной публикой. - person GManNickG; 11.07.2012