Po co używać idealnie przekazywanej wartości (functor)?

C++11 (i C++14) wprowadza dodatkowe konstrukcje językowe i ulepszenia, które dotyczą programowania ogólnego. Należą do nich takie cechy jak;

  • odniesienia do wartości R
  • Reference
  • Perfect forwarding
  • Przenieś semantykę, różne szablony i inne

Przeglądałem wcześniejszy szkic specyfikacji C++14 (teraz z zaktualizowanym tekstem) i kod w przykładzie w §20.5.1, compile-time integer sekwencje , które uznałem za interesujące i osobliwe.

template<class F, class Tuple, std::size_t... I>
decltype(auto) apply_impl(F&& f, Tuple&& t, index_sequence<I...>) {
  return std::forward<F>(f)(std::get<I>(std::forward<Tuple>(t))...);
}

template<class F, class Tuple>
decltype(auto) apply(F&& f, Tuple&& t) {
  using Indices = make_index_sequence<std::tuple_size<Tuple>::value>;
  return apply_impl(std::forward<F>(f), std::forward<Tuple>(t), Indices());
}

Online tutaj [intseq.ogólne] / 2 .

Pytanie

  • dlaczego funkcja f w apply_impl była przekazywana, tzn. dlaczego std::forward<F>(f)(std::get...?
  • Dlaczego po prostu nie zastosować funkcji jako f(std::get...?
Author: Niall, 2014-07-16

2 answers

W Skrócie...

TL;DR, chcesz zachować kategorię wartości (charakter r-wartość/l-wartość) functora, ponieważ może to wpłynąć na rozdzielczość przeciążenia , w szczególności członków kwalifikowanych ref .

Redukcja definicji funkcji

W związku z tym, że funkcja jest przekazywana, zmniejszyłem próbkę (i zmusiłem ją do kompilacji kompilatorem C++11) do;
template<class F, class... Args>
auto apply_impl(F&& func, Args&&... args) -> decltype(std::forward<F>(func)(std::forward<Args>(args)...)) {
  return std::forward<F>(func)(std::forward<Args>(args)...);
}

I tworzymy drugi formularz, w którym zastępujemy std::forward(func) Z just func;

template<class F, class... Args>
auto apply_impl_2(F&& func, Args&&... args) -> decltype(func(std::forward<Args>(args)...)) {
  return func(std::forward<Args>(args)...);
}

Ocena próbki

Ocena niektórych empirycznych dowodów na to, jak to się zachowuje (z zgodnymi kompilatorami) jest dobrym punktem wyjścia do oceny, dlaczego przykład kodu został napisany jako taki. Stąd dodatkowo zdefiniujemy funktor ogólny;

struct Functor1 {
  int operator()(int id) const
  {
    std::cout << "Functor1 ... " << id << std::endl;
    return id;
  }
};

Próbka wstępna

Uruchom jakiś przykładowy kod;

int main()
{
  Functor1 func1;
  apply_impl_2(func1, 1);
  apply_impl_2(Functor1(), 2);
  apply_impl(func1, 3);
  apply_impl(Functor1(), 4);
}

I wyjście jest zgodne z oczekiwaniami, niezależne od tego, czy użyto wartości r Functor1() czy wartości l func podczas wykonywania połączenia do apply_impl i apply_impl_2 wywoływany jest przeciążony operator połączenia. Jest wywoływana zarówno dla wartości r, jak i wartości l. W C++03, to było wszystko, co masz, nie można przeciążać metod prętowych opartych na" R-value-ness "lub" l-value-Ness " obiektu.

Functor1 ... 1
Functor1 ... 2
Functor1 ... 3
Functor1 ... 4

Próbki kwalifikowane Ref

Teraz musimy przeciążyć ten operator wywołania, aby rozciągnąć to a jeszcze trochę...

struct Functor2 {
  int operator()(int id) const &
  {
    std::cout << "Functor2 &... " << id << std::endl;
    return id;
  }
  int operator()(int id) &&
  {
    std::cout << "Functor2 &&... " << id << std::endl;
    return id;
  }
};

Uruchamiamy kolejny zestaw próbek;

int main()
{
  Functor2 func2;
  apply_impl_2(func2, 5);
  apply_impl_2(Functor2(), 6);
  apply_impl(func2, 7);
  apply_impl(Functor2(), 8);
}

A wyjście to;

Functor2 &... 5
Functor2 &.. 6
Functor2 &.. 7
Functor2 &&... 8

Dyskusja

W przypadku apply_impl_2 (id 5 i 6), Wyjście nie jest tak, jak początkowo można było się spodziewać. W obu przypadkach wywoływana jest wartość L operator() (wartość r w ogóle nie jest wywoływana). Można się było spodziewać, że ponieważ Functor2(), wartość r jest używana do wywołania apply_impl_2, wywołano by kwalifikowaną wartość R operator(). func, jako parametr nazwany do apply_impl_2, jest odniesieniem do wartości r, ale ponieważ jest nazwany, sam jest wartością l. Dlatego wartość L operator()(int) const& jest wywoływana zarówno w przypadku, gdy wartość L func2 jest argumentem, jak i wartość r Functor2() jest używana jako argument.

W przypadku apply_impl (id 7 Oraz 8)std::forward<F>(func) zachowuje lub zachowuje charakter wartości r/wartości l z argumentu podanego dla func. Dlatego wartość l kwalifikowana operator()(int) const& jest wywoływana z wartością l func2 używaną jako argument, a wartość R kwalifikowana operator()(int)&&, gdy wartość R Functor2() jest używana jako argument. Tego zachowania można się było spodziewać.

Wnioski

Użycie std::forward, poprzez doskonałe przekazywanie, zapewnia, że zachowamy naturę r-value/l-value oryginalnego argumentu dla func. Zachowuje ich wartość Kategoria .

Jest to wymagane, std::forward może i powinno być używane nie tylko do przekazywania argumentów do funkcji, ale także wtedy, gdy użycie argumentu jest wymagane, gdy należy zachować charakter r-wartość/l-wartość. Uwaga; są sytuacje, w których wartość r/L nie może lub nie powinna być zachowana, w takich sytuacjach std::forward nie powinna być używana(patrz converse poniżej).

Pojawia się wiele przykładów, które nieumyślnie tracą r-value/l-value charakter argumentów poprzez pozornie niewinne użycie odniesienia wartości r.

Zawsze trudno było napisać dobrze zdefiniowany i brzmiący kod ogólny. Wraz z wprowadzeniem odniesień o wartości r, a w szczególności odwołań, stało się możliwe napisanie lepszego kodu generycznego, bardziej zwięzłego, ale musimy być coraz bardziej świadomi tego, jaka jest oryginalna natura podanych argumentów i upewnić się, że są one zachowane, gdy używamy ich w generycznym kodzie kod, który piszemy.

Pełny przykładowy kod można znaleźć tutaj

Corollary i converse

  • następstwem pytania byłoby; biorąc pod uwagę załamanie odniesienia w funkcji szablonowej, w jaki sposób zachowuje się charakter wartości r/L argumentu? Odpowiedź-użyj std::forward<T>(t).
  • Converse; czy std::forward rozwiąże wszystkie Twoje problemy "uniwersalnego odniesienia"? Nie, są przypadki, w których nie powinno być używane , takie jak przekazywanie wartości więcej niż raz.

Krótkie tło do perfect forwarding

Doskonałe przekierowanie może być dla niektórych nieznane, więc czym jest doskonałe przekierowanie ?

W skrócie, perfect forwarding służy do zapewnienia, że argument dostarczony do funkcji jest przekazywany (przekazywany) do innej funkcji o tej samej kategorii wartości (zasadniczo r-wartość vs.l-wartość), jak pierwotnie dostarczone. Jest zwykle używany z funkcjami szablonu, gdzie odniesienie / align = "center" bgcolor = "# e0ffe0 " / cesarz chin / / align = center /

W 2013 roku, w trakcie prezentacji going Native 2013, Scott Meyers podał następujący pseudo kod, aby wyjaśnić działanie std::forward (około 20 minut); {[56]]}
template <typename T>
T&& forward(T&& param) { // T&& here is formulated to disallow type deduction
  if (is_lvalue_reference<T>::value) {
    return param; // return type T&& collapses to T& in this case
  }
  else {
    return move(param);
  }
}
W języku C++11 istnieje kilka podstawowych konstrukcji językowych, które tworzą podstawy dla wielu z tego, co widzimy obecnie w programowaniu ogólnym:]}
  • odwołanie
  • rvalue references
  • Move semantyka
[49]}użycie std::forward jest obecnie zamierzone w formule std::forward<T>, zrozumienie jak działa std::forward pomaga zrozumieć, dlaczego tak jest, a także pomaga w identyfikacji Nie-idiomatycznego lub nieprawidłowego użycia wartości R, załamania odniesienia i ilk.

Thomas Becker zapewnia miły, ale gęsty zapis na idealne przekierowanie problem i rozwiązanie .

Czym są kwalifikatory ref?

Ref-qualifiers (lvalue ref-qualifier & i rvalue ref-qualifier &&) są podobne do kwalifikatorów cv, ponieważ (ref-Qualifiers ) są używane podczas overload resolution do określenia, którą metodę wywołać. Zachowują się tak, jak byś tego oczekiwał; & stosuje się do wartości LV, A && do wartości R. Uwaga: w przeciwieństwie do kwalifikacji cv, *this pozostaje wyrażeniem wartości L.

 46
Author: Niall,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:00:10

Oto praktyczny przykład.

struct concat {
  std::vector<int> state;
  std::vector<int> const& operator()(int x)&{
    state.push_back(x);
    return state;
  }
  std::vector<int> operator()(int x)&&{
    state.push_back(x);
    return std::move(state);
  }
  std::vector<int> const& operator()()&{ return state; }
  std::vector<int> operator()()&&{ return std::move(state); }
};

Ten obiekt funkcji przyjmuje x i łączy go z wewnętrznym std::vector. Następnie zwraca std::vector.

Jeśli jest oceniana w kontekście rvalue, to movejest tymczasowa, w przeciwnym razie zwraca const& do wektora wewnętrznego.

Teraz nazywamy apply:

auto result = apply( concat{}, std::make_tuple(2) );

Ponieważ starannie przekazaliśmy obiekt funkcji, przydzielany jest tylko 1 std::vector bufor. Jest po prostu przeniesiony do result.

BEZ ostrożności przekierowanie, w końcu tworzymy wewnętrzny std::vector i kopiujemy go do result, a następnie odrzucamy wewnętrzny std::vector.

Ponieważ operator()&& wie, że obiekt funkcji powinien być traktowany jako wartość R, która ma zostać zniszczona, może wyrwać wnętrzności z obiektu funkcji podczas jego działania. operator()& nie może tego zrobić.

Staranne wykorzystanie perfekcyjnego przekazywania obiektów funkcyjnych umożliwia taką optymalizację.

Zauważ jednak, że jest bardzo mało wykorzystania tej techniki "w dziczy" w tym momencie. Przeciążenie kwalifikowane rvalue jest niejasne i robi to operator() moreso.

Mogłem łatwo zobaczyć przyszłe wersje C++ automatycznie używając stanu rvalue lambda do niejawnie move jego przechwytywanych przez wartość danych w pewnych kontekstach.

 13
Author: Yakk - Adam Nevraumont,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-02-24 04:01:47