Ответ 1
Люди делают идеальные конструкторы вперед.
Есть затраты.
Во-первых, стоимость заключается в том, что они должны быть в файле заголовка. Во-вторых, каждое использование имеет тенденцию приводить к созданию другого конструктора. В-третьих, вы не можете использовать синтаксис инициализации {}
для объектов, из которых вы строите.
В-четвертых, он плохо взаимодействует с конструкторами Foo(Foo const&)
и Foo(Foo&&)
. Он не заменит их (из-за языковых правил), но он будет выбран над ними для Foo(Foo&)
. Это можно исправить небольшим количеством шаблона SFINAE:
template<class T,
std::enable_if_t<!std::is_same<std::decay_t<T>, Foo>{},int> =0
>
Foo(T&& bar) : bar_{std::forward<T>(bar)} {};
который теперь больше не предпочтительнее Foo(Foo const&)
для аргументов типа Foo&
. Пока мы находимся в этом, мы можем сделать:
Bar bar_;
template<class T,
std::enable_if_t<!std::is_same<std::decay_t<T>, Foo>{},int> =0,
std::enable_if_t<std::is_constructible<Bar, T>{},int> =0
>
Foo(T&& bar) :
bar_{std::forward<T>(bar)}
{};
и теперь этот конструктор работает, только если аргумент может быть использован для построения bar
.
Следующее, что вам нужно сделать, - либо поддерживать конструкцию стиля {}
конструкции bar
, либо кусочную конструкцию, либо конструкцию varargs, где вы переходите в панель.
Вот вариант varargs:
Bar bar_;
template<class T0, class...Ts,
std::enable_if_t<sizeof...(Ts)||!std::is_same<std::decay_t<T0>, Foo>{},int> =0,
std::enable_if_t<std::is_constructible<Bar, T0, Ts...>{},int> =0
>
Foo(T0&&t0, Ts&&...ts) :
bar_{std::forward<T0>(t0), std::forward<Ts>(ts)...}
{};
Foo()=default;
С другой стороны, если мы добавим:
Foo(Bar&& bin):bar_(std::move(bin));
теперь поддерживаем синтаксис Foo( {construct_bar_here} )
, что приятно. Однако это не требуется, если у нас уже есть вышеуказанный вариант (или подобная кусочная конструкция). Тем не менее, иногда список инициализаторов хорош для пересылки, особенно если мы не знаем тип bar_
при написании кода (например, generics):
template<class T0, class...Ts,
std::enable_if_t<std::is_constructible<Bar, std::initializer_list<T0>, Ts...>{},int> =0
>
Foo(std::initializer_list<T0> t0, Ts&&...ts) :
bar_{t0, std::forward<Ts>(ts)...}
{};
поэтому, если bar
является std::vector<int>
, мы можем сделать Foo( {1,2,3} )
и в итоге {1,2,3}
внутри bar_
.
На этом этапе вам стоит задаться вопросом: "Почему я просто не написал Foo(Bar)
". Неужели так дорого перемещать bar
?
В общем коде библиотеки -экспекции вы захотите зайти так далеко, как указано выше. Но очень часто ваши объекты известны и дешевы для перемещения. Поэтому напишите действительно простой, верный, Foo(Bar)
и сделайте все с помощью tomfoolery.
Существует случай, когда у вас есть N переменных, которые не являются дешевыми для перемещения, и вы хотите эффективности, и вы не хотите помещать реализацию в файл заголовка.
Затем вы просто пишете создателя стирания bar
, который берет все, что может быть использовано для создания bar
либо напрямую, либо через std::make_from_tuple
и сохранит создание на более позднюю дату. Затем он использует RVO для прямой сборки bar
на месте в целевом местоположении.
template<class T>
struct make {
using maker_t = T(*)(void*);
template<class Tuple>
static maker_t make_tuple_maker() {
return [](void* vtup)->T{
return make_from_tuple<T>( std::forward<Tuple>(*static_cast<std::remove_reference_t<Tuple>*>(vtup)) );
};
}
template<class U>
static maker_t make_element_maker() {
return [](void* velem)->T{
return T( std::forward<U>(*static_cast<std::remove_reference_t<U>*>(velem)) );
};
}
void* ptr = nullptr;
maker_t maker = nullptr;
template<class U,
std::enable_if_t< std::is_constructible<T, U>{}, int> =0,
std::enable_if_t<!std::is_same<std::decay_t<U>, make>{}, int> =0
>
make( U&& u ):
ptr( (void*)std::addressof(u) ),
maker( make_element_maker<U>() )
{}
template<class Tuple,
std::enable_if_t< !std::is_constructible<T, Tuple>{}, int> =0,
std::enable_if_t< !std::is_same<std::decay_t<Tuple>, make>{}, int> =0,
std::enable_if_t<(0<=std::tuple_size<std::remove_reference_t<Tuple>>{}), int> = 0 // SFINAE test that Tuple is a tuple-like
// TODO: SFINAE test that using Tuple to construct T works
>
make( Tuple&& tup ):
ptr( std::addressof(tup) ),
maker( make_tuple_maker<Tuple>() )
{}
T operator()() const {
return maker(ptr);
}
};
В коде используется функция С++ 17, std::make_from_tuple
, которую относительно легко написать в С++ 11. В С++ 17 гарантированный elision означает, что он даже работает с недвижущимися типами, что действительно круто.
Теперь вы можете написать:
Foo( make<Bar> bar_in ):bar_( bar_in() ) {}
и тело Foo::Foo
может быть перемещено из файла заголовка.
Но это более сумасшествие, чем вышеупомянутые альтернативы.
Опять же, рассмотрели ли вы просто запись Foo(Bar)
?