Ich habe den zweiten Teil von Walter Browns CppCon2014-Vortrag über Template-Metaprogrammierung gesehen , in dem er die Verwendung seiner neuartigen void_t<>
Konstruktion diskutierte . Während seines Vortrags stellte Peter Sommerlad ihm eine Frage, die ich nicht ganz verstand. (Link geht direkt auf die Frage, der diskutierte Code fand direkt davor statt)
Fragte Sommerlad
Walter, würde das bedeuten, dass wir jetzt tatsächlich Konzepte lite implementieren können?
worauf Walter antwortete
Oh ja! Ich habe es geschafft ... Es hat nicht ganz die gleiche Syntax.
Ich habe verstanden, dass es bei diesem Austausch um Concepts Lite geht. Ist dieses Muster wirklich das ? vielseitig? Aus irgendeinem Grund sehe ich es nicht. Kann jemand erklären (oder skizzieren), wie so etwas aussehen könnte? Geht es hier nur um enable_if
und um Merkmale zu definieren, oder worauf bezog sich der Fragesteller?
Die void_t
Vorlage ist wie folgt definiert:
template<class ...> using void_t = void;
Er verwendet dies dann, um festzustellen, ob Typanweisungen gut geformt sind, und verwendet dies, um das is_copy_assignable
Typmerkmal zu implementieren :
//helper type
template<class T>
using copy_assignment_t
= decltype(declval<T&>() = declval<T const&>());
//base case template
template<class T, class=void>
struct is_copy_assignable : std::false_type {};
//SFINAE version only for types where copy_assignment_t<T> is well-formed.
template<class T>
struct is_copy_assignable<T, void_t<copy_assignment_t<T>>>
: std::is_same<copy_assignment_t<T>,T&> {};
Aufgrund des Gesprächs verstehe ich, wie dieses Beispiel funktioniert, aber ich sehe nicht, wie wir von hier zu etwas wie Concepts Lite gelangen.
quelle
enable_if
mit leicht zugänglichen und leicht zu beschreibenden Einschränkungen und Typmerkmalen versehen, oder ist es etwas komplizierter?Antworten:
Ja, Konzepte lite verkleiden SFINAE im Grunde. Außerdem ermöglicht es eine tiefere Selbstbeobachtung, um eine bessere Überlastung zu ermöglichen. Dies funktioniert jedoch nur, wenn die Konzeptprädikate als definiert sind
concept bool
. Die verbesserte Überladung funktioniert nicht mit den aktuellen Konzeptprädikaten, es kann jedoch eine bedingte Überladung verwendet werden. Schauen wir uns an, wie wir in C ++ 14 Prädikate definieren, Vorlagen einschränken und Funktionen überladen können. Dies ist ziemlich lang, aber es wird erläutert, wie alle Tools erstellt werden, die erforderlich sind, um dies in C ++ 14 zu erreichen.Prädikate definieren
Erstens ist es irgendwie hässlich, das Prädikat mit all dem
std::declval
unddecltype
überall zu lesen . Stattdessen können wir die Tatsache ausnutzen, dass wir eine Funktion mithilfe eines abschließenden Dekltyps (aus Eric Nieblers Blog-Beitrag hier ) wie folgt einschränken können:struct Incrementable { template<class T> auto requires_(T&& x) -> decltype(++x); };
Wenn dies
++x
nicht gültig ist, kann dierequires_
Member-Funktion nicht aufgerufen werden. So können wir einmodels
Merkmal erstellen , das nur prüft, obrequires_
es aufrufbar ist, indemvoid_t
:template<class Concept, class Enable=void> struct models : std::false_type {}; template<class Concept, class... Ts> struct models<Concept(Ts...), void_t< decltype(std::declval<Concept>().requires_(std::declval<Ts>()...)) >> : std::true_type {};
Vorlagen einschränken
Wenn wir also die Vorlage basierend auf dem Konzept einschränken möchten, müssen wir sie weiterhin verwenden
enable_if
, aber wir können dieses Makro verwenden, um sie sauberer zu machen:#define REQUIRES(...) typename std::enable_if<(__VA_ARGS__), int>::type = 0
So können wir eine
increment
Funktion definieren , die basierend auf demIncrementable
Konzept eingeschränkt ist:template<class T, REQUIRES(models<Incrementable(T)>())> void increment(T& x) { ++x; }
Wenn wir also
increment
mit etwas anrufen , das nicht istIncrementable
, erhalten wir einen Fehler wie den folgenden:test.cpp:23:5: error: no matching function for call to 'incrementable' incrementable(f); ^~~~~~~~~~~~~ test.cpp:11:19: note: candidate template ignored: disabled by 'enable_if' [with T = foo] template<class T, REQUIRES(models<Incrementable(T)>())> ^
Überladefunktionen
Wenn wir nun eine Überladung durchführen möchten, möchten wir eine bedingte Überladung verwenden. Angenommen, wir möchten ein
std::advance
Prädikat für die Verwendung von Konzepten erstellen. Wir könnten es folgendermaßen definieren (im Moment werden wir den dekrementierbaren Fall ignorieren):struct Incrementable { template<class T> auto requires_(T&& x) -> decltype(++x); }; struct Advanceable { template<class T, class I> auto requires_(T&& x, I&& i) -> decltype(x += i); }; template<class Iterator, REQUIRES(models<Advanceable(Iterator, int)>())> void advance(Iterator& it, int n) { it += n; } template<class Iterator, REQUIRES(models<Incrementable(Iterator)>())> void advance(Iterator& it, int n) { while (n--) ++it; }
Dies führt jedoch zu einer mehrdeutigen Überladung (in Konzepten lite wäre dies immer noch eine mehrdeutige Überladung, es sei denn, wir ändern unsere Prädikate so, dass sie auf die anderen Prädikate in a verweisen
concept bool
), wenn sie mit demstd::vector
Iterator verwendet werden. Wir möchten die Anrufe bestellen, was wir durch bedingte Überladung tun können. Man kann sich vorstellen, so etwas zu schreiben (was in C ++ nicht gültig ist):template<class Iterator> void advance(Iterator& it, int n) if (models<Advanceable(Iterator, int)>()) { it += n; } else if (models<Incrementable(Iterator)>()) { while (n--) ++it; }
Wenn also die erste Funktion nicht aufgerufen wird, wird die nächste Funktion aufgerufen. Beginnen wir also damit, es für zwei Funktionen zu implementieren. Wir werden eine Klasse namens erstellen,
basic_conditional
die zwei Funktionsobjekte als Vorlagenparameter akzeptiert:struct Callable { template<class F, class... Ts> auto requires_(F&& f, Ts&&... xs) -> decltype( f(std::forward<Ts>(xs)...) ); }; template<class F1, class F2> struct basic_conditional { // We don't need to use a requires clause here because the trailing // `decltype` will constrain the template for us. template<class... Ts> auto operator()(Ts&&... xs) -> decltype(F1()(std::forward<Ts>(xs)...)) { return F1()(std::forward<Ts>(xs)...); } // Here we add a requires clause to make this function callable only if // `F1` is not callable. template<class... Ts, REQUIRES(!models<Callable(F1, Ts&&...)>())> auto operator()(Ts&&... xs) -> decltype(F2()(std::forward<Ts>(xs)...)) { return F2()(std::forward<Ts>(xs)...); } };
Das bedeutet also, dass wir unsere Funktionen stattdessen als Funktionsobjekte definieren müssen:
struct advance_advanceable { template<class Iterator, REQUIRES(models<Advanceable(Iterator, int)>())> void operator()(Iterator& it, int n) const { it += n; } }; struct advance_incrementable { template<class Iterator, REQUIRES(models<Incrementable(Iterator)>())> void operator()(Iterator& it, int n) const { while (n--) ++it; } }; static conditional<advance_advanceable, advance_incrementable> advance = {};
Wenn wir also versuchen, es mit einem zu verwenden
std::vector
:std::vector<int> v = { 1, 2, 3, 4, 5, 6 }; auto iterator = v.begin(); advance(iterator, 4); std::cout << *iterator << std::endl;
Es wird kompiliert und ausgedruckt
5
.Hat jedoch
std::advance
tatsächlich drei Überladungen, so dass wir das verwenden können, umbasic_conditional
zu implementierenconditional
, das für eine beliebige Anzahl von Funktionen unter Verwendung der Rekursion funktioniert:template<class F, class... Fs> struct conditional : basic_conditional<F, conditional<Fs...>> {}; template<class F> struct conditional<F> : F {};
Jetzt können wir das Ganze so schreiben
std::advance
:struct Incrementable { template<class T> auto requires_(T&& x) -> decltype(++x); }; struct Decrementable { template<class T> auto requires_(T&& x) -> decltype(--x); }; struct Advanceable { template<class T, class I> auto requires_(T&& x, I&& i) -> decltype(x += i); }; struct advance_advanceable { template<class Iterator, REQUIRES(models<Advanceable(Iterator, int)>())> void operator()(Iterator& it, int n) const { it += n; } }; struct advance_decrementable { template<class Iterator, REQUIRES(models<Decrementable(Iterator)>())> void operator()(Iterator& it, int n) const { if (n > 0) while (n--) ++it; else { n *= -1; while (n--) --it; } } }; struct advance_incrementable { template<class Iterator, REQUIRES(models<Incrementable(Iterator)>())> void operator()(Iterator& it, int n) const { while (n--) ++it; } }; static conditional<advance_advanceable, advance_decrementable, advance_incrementable> advance = {};
Überladung mit Lambdas
Zusätzlich könnten wir jedoch Lambdas verwenden, um es anstelle von Funktionsobjekten zu schreiben, was dazu beitragen kann, das Schreiben sauberer zu gestalten. Wir verwenden dieses
STATIC_LAMBDA
Makro also, um Lambdas zur Kompilierungszeit zu erstellen:struct wrapper_factor { template<class F> constexpr wrapper<F> operator += (F*) { return {}; } }; struct addr_add { template<class T> friend typename std::remove_reference<T>::type *operator+(addr_add, T &&t) { return &t; } }; #define STATIC_LAMBDA wrapper_factor() += true ? nullptr : addr_add() + []
Und fügen Sie eine
make_conditional
Funktion hinzu, die istconstexpr
:template<class... Fs> constexpr conditional<Fs...> make_conditional(Fs...) { return {}; }
Dann können wir die
advance
Funktion jetzt so schreiben :constexpr const advance = make_conditional( STATIC_LAMBDA(auto& it, int n, REQUIRES(models<Advanceable(decltype(it), int)>())) { it += n; }, STATIC_LAMBDA(auto& it, int n, REQUIRES(models<Decrementable(decltype(it))>())) { if (n > 0) while (n--) ++it; else { n *= -1; while (n--) --it; } }, STATIC_LAMBDA(auto& it, int n, REQUIRES(models<Incrementable(decltype(it))>())) { while (n--) ++it; } );
Das ist wenig kompakter und lesbarer als die Verwendung der Funktionsobjektversionen.
Zusätzlich könnten wir eine
modeled
Funktion definieren , um diedecltype
Hässlichkeit zu reduzieren :template<class Concept, class... Ts> constexpr auto modeled(Ts&&...) { return models<Concept(Ts...)>(); } constexpr const advance = make_conditional( STATIC_LAMBDA(auto& it, int n, REQUIRES(modeled<Advanceable>(it, n))) { it += n; }, STATIC_LAMBDA(auto& it, int n, REQUIRES(modeled<Decrementable>(it))) { if (n > 0) while (n--) ++it; else { n *= -1; while (n--) --it; } }, STATIC_LAMBDA(auto& it, int n, REQUIRES(modeled<Incrementable>(it))) { while (n--) ++it; } );
Schließlich, wenn Sie daran interessiert sind, vorhandene Bibliothekslösungen zu verwenden (anstatt Ihre eigenen zu rollen, wie ich gezeigt habe). Es gibt die Tick- Bibliothek, die ein Framework zum Definieren von Konzepten und zum Einschränken von Vorlagen bietet. Und die Fit- Bibliothek kann die Funktionen und das Überladen übernehmen.
quelle
Ts...
die Funktionsparameter undConcept
der Rückgabetyp des Funktionstyps übereinstimmen.