Ludzie robią doskonałe konstruktory do przodu.
Istnieją koszty.
Po pierwsze, koszt jest taki, że muszą znajdować się w pliku nagłówkowym. Po drugie, każde użycie prowadzi do utworzenia innego konstruktora. Po trzecie, nie można użyć składni inicjowania w stylu {}
dla obiektów, z których tworzysz.
Po czwarte, słabo współpracuje z konstruktorami Foo(Foo const&)
i Foo(Foo&&)
. Nie zastąpi ich (ze względu na reguły językowe), ale zostanie nad nimi zaznaczony jako Foo(Foo&)
. To może być ustalona z odrobiną boilerplate 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)} {};
który teraz już nie jest preferowany przez Foo(Foo const&)
dla argumentów typu Foo&
. Mimo, że jesteśmy na to, co możemy zrobić:
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)}
{};
a teraz to konstruktor działa tylko wtedy, gdy argument może być wykorzystane do skonstruowania bar
.
Następną rzeczą, którą będziesz chciał zrobić, to albo wesprzeć konstrukcję w stylu {}
w konstrukcji bar
, albo w konstrukcji kawałkowej, albo w konstrukcji varargs, w której przesuwasz pasek w prawo.
Oto wariant 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;
Z drugiej strony, jeśli dodamy:
Foo(Bar&& bin):bar_(std::move(bin));
teraz wspierać Foo({construct_bar_here})
składni, który jest miły. Jednak nie jest to wymagane, jeśli mamy już powyższe varardic (lub podobne konstrukcje w kawałkach).Mimo to, czasami lista inicjująca jest miły do przekazania, szczególnie jeśli nie znamy typ bar_
gdy piszemy kod (rodzajowych, powiedzmy):
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)...}
{};
więc jeśli Bar
jest std::vector<int>
możemy zrobić Foo({1,2,3})
i kończy się na {1,2,3}
w ciągu bar_
.
W tym momencie musisz się zastanawiać "dlaczego nie napisałem po prostu Foo(Bar)
". Czy to naprawdę drogie, aby przenieść Bar
?
W ogólnym kodzie biblioteki-esque, będziesz chciał pójść tak daleko jak wyżej. Ale bardzo często twoje obiekty są zarówno znane, jak i tanie w przenoszeniu. Napisz więc naprawdę proste, raczej poprawne, Foo(Bar)
i wykonaj całą grę.
Istnieje przypadek, w którym masz N zmiennych, które nie są tanie w przenoszeniu i chcesz wydajności, a nie chcesz umieścić implementacji w pliku nagłówkowym.
Następnie wystarczy napisać typ kasowania Bar
twórca że zaczyna coś, które mogą być wykorzystane do stworzenia Bar
bezpośrednio lub poprzez std::make_from_tuple
i przechowuje utworzenie na późniejszym terminie. Następnie używa on RVO do bezpośredniego konstruowania lokacji Bar
w miejscu docelowym.
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);
}
};
Kod wykorzystuje C++ 17 funkcji, std::make_from_tuple
, który jest stosunkowo łatwy do pisania w C++ 11. W elemencie gwarantowanym C++ 17 oznacza to, że działa nawet z typami nieruchowymi, co jest naprawdę fajne.
Live example.
Teraz można napisać:
Foo(make<Bar> bar_in):bar_(bar_in()) {}
i ciało Foo::Foo
mogą być przenoszone z pliku nagłówka.
Ale to jest bardziej szalone niż powyższe alternatywy.
Jeszcze raz, czy rozważałeś właśnie napisanie Foo(Bar)
?
Szablon nie zezwala na niejawne konwersje, np. od aparatu ortodontycznego. Czasami dobrze jest wcześnie dokonać konwersji na żądany typ. –
@KerrekSB Odpowiedź, proszę, chcę się dowiedzieć. –
Można również użyć 'typename = std :: enable_if_t, Bar >>', aby ograniczyć typ. –