2016-09-22 14 views
16

Według mojej wiedzy, dwa popularne sposoby efektywnego wdrożenia konstruktora w C++ 11 używasz dwóch z nichKonstruktor za pomocą std :: naprzód

Foo(const Bar& bar) : bar_{bar} {}; 
Foo(Bar&& bar)  : bar_{std::move(bar)} {}; 

lub tylko jeden w modzie

Foo(Bar bar) : bar_{std::move(bar)} {}; 

z pierwszą opcją zapewniającą optymalną wydajność (np. Jedną kopię w przypadku wartości l i jednego ruchu w przypadku wartości r), ale wymagającą 2 N przeciążenia dla zmiennych N, podczas gdy druga opcja wymaga tylko jedna funkcja kosztem dodatkowy ruch podczas przejścia w l-wartości.

W większości przypadków nie powinno to mieć większego znaczenia, ale z pewnością żaden z nich nie jest optymalny. Jednak można również wykonać następujące czynności:

template<typename T> 
Foo(T&& bar) : bar_{std::forward<T>(bar)} {}; 

ten ma tę wadę, że pozwala na zmienne potencjalnie niechcianych typów jako parametr bar (co jest problemem, jestem pewien, że jest łatwo rozwiązać za pomocą szablonu specjalizacji), ale w żadnym Sprawa wydajności jest optymalna, a kod rośnie liniowo wraz z ilością zmiennych.

Dlaczego nikt nie używa czegoś podobnego do tego celu? Czy nie jest to najbardziej optymalny sposób?

+3

Szablon nie zezwala na niejawne konwersje, np. od aparatu ortodontycznego. Czasami dobrze jest wcześnie dokonać konwersji na żądany typ. –

+0

@KerrekSB Odpowiedź, proszę, chcę się dowiedzieć. –

+0

Można również użyć 'typename = std :: enable_if_t , Bar >>', aby ograniczyć typ. –

Odpowiedz

21

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)?

+3

Możesz mieć inicjację '{}' -like z odwołaniami uniwersalnymi: 'template Foo (T && bar): ...'. Gdy argument ma typ, jest używany. W przeciwnym razie kompilator wybierze wartość domyślną i stworzy dla ciebie wartość rastru 'Bar'. –

Powiązane problemy