5

Przed C++ 11, kiedyś napisać kod jak poniżej:Właściwe stosowanie uniwersalnych odniesień

// Small functions 
void doThingsWithA(const A& a) 
{ 
    // do stuff 
} 

void doThingsWithB(const B& b) 
{ 
    // do stuff 
} 

void doThingsWithC(const C& c) 
{ 
    // do stuff 
} 

// Big function 
void doThingsWithABC(const A& a, const B& b, const C& c) 
{ 
    // do stuff 
    doThingsWithA(a); 
    doThingsWithB(b); 
    doThingsWithC(c); 
    // do stuff 
} 

Ale teraz, z semantyką poruszać, może okazać się interesujące (przynajmniej w niektórych przypadkach), aby umożliwić mój funkcje wziąć referencje rvalue jako parametry i dodać te przeciążeń:

void doThingsWithA(A&& a); 
void doThingsWithB(B&& b); 
void doThingsWithC(C&& c); 

Z czego wnoszę, jeśli chcę, aby móc nazwać tych przeciążeń w moim wielkim funkcji, trzeba użyć doskonałe przekazywanie, który może wyglądać to (jest to nieco mniej czytelne, ale myślę, że może być ok z dobrym nazewnictwem nia dla typów szablonu):

template<typename TplA, typename TplB, typename TplC> 
void doThingsWithABC(TplA&& a, TplB&& b, TplC&& c) 
{ 
    // do stuff 
    doThingsWithA(std::forward<TplA>(a)); 
    doThingsWithB(std::forward<TplB>(b)); 
    doThingsWithC(std::forward<TplC>(c)); 
    // do stuff 
} 

Mój problem jest taki: nie oznacza to, że jeśli moje małe funkcje mają inne przeciążeń, stanie się możliwe, aby wywołać wielki jeden z parametrów typów, dla których to było nie przeznaczone?

myślę, że to może działać, aby zapobiec tego:

template<typename TplA, typename TplB, typename TplC, 
class = typename std::enable_if<std::is_same<A, std::decay<TplA>::type>::value>::type, 
class = typename std::enable_if<std::is_same<B, std::decay<TplB>::type>::value>::type, 
class = typename std::enable_if<std::is_same<C, std::decay<TplC>::type>::value>::type> 
    doThingsWithABC(TplA&& a, TplB&& b, TplC&& c) 
{ 
    // do stuff 
    doThingsWithA(std::forward<TplA>(a)); 
    doThingsWithB(std::forward<TplB>(b)); 
    doThingsWithC(std::forward<TplC>(c)); 
    // do stuff 
} 

Chociaż nie jestem pewien, czy to nie jest zbyt restrykcyjne, ponieważ nie mam pojęcia, jak się zachowuje, gdy próbuję zadzwonić wielkie funkcje z typami, które są domyślnie wymienialne na A, B lub C ...

Ale ... nawet zakładając, że to działa, czy naprawdę nie mam innych opcji? (Mam na myśli ... to nie jest łatwe w oczach)

+3

Możesz użyć 'static_assert', jeśli jest to łatwiejsze dla twoich oczu. –

+0

Możesz użyć makra do wygenerowania wszystkich 8 wersji 'doThingsWithABC' ... * kaczek * – Brian

Odpowiedz

6

Idealne spedycja jest głównie do kiedy nie wiem, w jaki sposób dane będą spożywane, bo piszesz rodzajowe otoki danych "użytkownik dostarczone.

W prostym, opisanym powyżej proceduralnym systemie, 3 rzeczy, które wykonujesz, będą konkretnymi zadaniami.

Oznacza to, że będziesz wiedział, czy skorzystają z posiadania ruchomego źródła danych i czy mają one sens, jeśli muszą skopiować, a ruch jest tani.

Jeśli kopiowanie ma sens, ale ruch jest szybszy, a ruch jest tani (typowy przypadek), powinien przyjmować parametry według wartości i przenosić je, gdy przechowują swoją lokalną kopię.

Ta reguła następnie stosuje się rekurencyjnie do funkcji, która wywołuje 3 funkcje podrzędne.

Jeśli funkcja nie korzysta z przeprowadzki, pobierz ją przez const&.

Jeśli kopiowanie nie ma sensu, pobierz według wartości rwartej (nie odniesienia uniwersalnego) lub według wartości.

W przypadku, gdy jest to zarówno dobra, aby móc moveimove pozostaje drogie należy rozważyć doskonałe przekazywanie.Jak wspomniano powyżej, dzieje się tak zazwyczaj tylko podczas zawijania funkcji ustawianych przez "użytkownika" twojej bazy kodu, ponieważ zwykle move jest naprawdę bardzo tanie lub tak drogie, jak kopiowanie. Musisz być na poziomie pośrednim lub nieokreślonym na poziomie wydajności, by perfekcyjne przekazywanie było opłacalne.

Istnieją inne zastosowania dla idealnego przekazywania, takie jak mutatory kontenerów, ale są bardziej ezoteryczne. Na przykład mój mutator z zakresu backwards perfekcyjnie przekazuje przychodzący zakres do pamięci masowej, aby mieć przedłużenie okresu istnienia odniesienia działającego poprawnie, gdy łańcuchy mutatorów z wieloma zakresami są w pętlach w stylu C++ 11 o zasięgu w postaci for(:).

Szalenie perfekcyjne przekazywanie powoduje wygenerowanie nadpisanego kodu, powolne kompilacje, nieszczelne implementacje i trudny do zrozumienia kod.

5

Skorzystaj z static_assert zamiast z. IMHO, ta opcja jest nie tylko łatwiejsza dla oczu, ale również bardziej przyjazna dla użytkownika. Kompilator wydrukuje wyraźny komunikat o błędzie, jeśli typy argumentów zostaną naruszone, podczas gdy z odpowiednikiem enable_if będzie skarżył się, że nie znaleziono pasującej funkcji.

template<typename TplA, typename TplB, typename TplC> 
void doThingsWithABC(TplA&& a, TplB&& b, TplC&& c) 
{ 
    static_assert(std::is_same<A, std::decay<TplA>::type>::value, "arg1 must be of type A"); 
    static_assert(std::is_same<B, std::decay<TplB>::type>::value, "arg2 must be of type B"); 
    static_assert(std::is_same<C, std::decay<TplC>::type>::value, "arg3 must be of type C"); 
    // do stuff 
    doThingsWithA(std::forward<TplA>(a)); 
    doThingsWithB(std::forward<TplB>(b)); 
    doThingsWithC(std::forward<TplC>(c)); 
    // do stuff 
} 
+0

' static_assert' jest rzeczywiście bardziej czytelny. Jednak nie zadziała, gdy wystąpią inne przeciążenia funkcji. W takim przypadku opcja "enable_if" (SFINAE) jest jedyną opcją. –