void_t "kann Konzepte implementieren"?

68

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_ifund um Merkmale zu definieren, oder worauf bezog sich der Fragesteller?

Die void_tVorlage 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_assignableTypmerkmal 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.

Tim Seguine
quelle
2
Interessante Frage, leider unterscheidet sie sich nicht wirklich von der Variante "Bitte schauen Sie sich meinen Quellcode an, der auf (einem Repository) gehostet wird". Nur schlimmer, weil es ein Video ist. Könnten Sie Ihrer Frage möglicherweise relevante Codefragmente hinzufügen, damit sie eigenständiger wird? Geben Sie mindestens die Zeit in dem Video an, über das Sie sprechen. "Der zweite Teil" ist fast nutzlos.
Ben Voigt
1
@ BenVoigt Okay, ich habe die Frage aktualisiert. Der Youtube-Link enthält einen Zeitcode, der direkt zur Frage springt. Und ich habe das Hauptbeispiel hinzugefügt, das er in seiner Präsentation verwendet hat. Ich hoffe, das hat die Frage klarer gemacht.
Tim Seguine
4
Von dort gelangen Sie nicht zu Concepts Lite. Es ist eine coole Technik und Sie können sie für nette "require" -Klauseln in Vorlagenparameterlisten verwenden, aber Sie können den Compiler nicht dazu bringen, Vorlagen eine Teilreihenfolge aufzuerlegen, basierend darauf, welches "Konzept" verfeinert wird. Sie können auch keine Anforderungen an Nicht-Vorlagenelementfunktionen einer Klassenvorlage oder an einige der anderen völlig neuen Sprachfunktionen stellen , die Teil des Concepts TS sind.
Jonathan Wakely
1
@JonathanWakely okay, sind diese "erforderlichen" Klauseln, von denen Sie sprechen (unsere gefälschten), nur enable_ifmit leicht zugänglichen und leicht zu beschreibenden Einschränkungen und Typmerkmalen versehen, oder ist es etwas komplizierter?
Tim Seguine
4
Ja, ich glaube, es ist einfach eine sehr coole Art, SFINAE zu machen. Concepts Lite ist viel mehr als SFINAE, es ersetzt SFINAE vollständig , anstatt es mit einer schönen Syntax zu versehen.
Jonathan Wakely

Antworten:

118

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::declvalund decltypeü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 ++xnicht gültig ist, kann die requires_Member-Funktion nicht aufgerufen werden. So können wir ein modelsMerkmal erstellen , das nur prüft, ob requires_es aufrufbar ist, indem void_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 incrementFunktion definieren , die basierend auf dem IncrementableKonzept eingeschränkt ist:

template<class T, REQUIRES(models<Incrementable(T)>())>
void increment(T& x)
{
    ++x;
}

Wenn wir also incrementmit etwas anrufen , das nicht ist Incrementable, 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::advancePrä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 dem std::vectorIterator 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_conditionaldie 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::advancetatsächlich drei Überladungen, so dass wir das verwenden können, um basic_conditionalzu implementieren conditional, 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_LAMBDAMakro 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_conditionalFunktion hinzu, die ist constexpr:

template<class... Fs>
constexpr conditional<Fs...> make_conditional(Fs...)
{
    return {};
}

Dann können wir die advanceFunktion 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 modeledFunktion definieren , um die decltypeHä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.

Paul Fultz II
quelle
2
Vielen Dank, dass Sie sich die Zeit genommen haben, eine so ausführliche Antwort zu schreiben. Dies ist mehr als ich erwartet hatte, aber genau die Art von Antwort, auf die ich gehofft hatte.
Tim Seguine
1
Aufgrund der hohen Qualität und Quantität positiv bewertet ... aber OMG, meine Übelkeit in C ++ 11 tritt ein.
Jason S
5
FWIW, der Trick zur Implementierung von STATIC_LAMBDA ist nicht standardkonformes C ++. Es funktioniert in der Praxis, weil ein System ziemlich seltsam sein müsste, damit es nicht funktioniert, aber das macht es nicht 100% koscher.
Charphacy
1
@mattnewport Ja, es entspricht einem Funktionstyp, bei dem Ts...die Funktionsparameter und Conceptder Rückgabetyp des Funktionstyps übereinstimmen.
Paul Fultz II
2
Ah, ok, ich glaube ich verstehe es jetzt. Was mich warf, war nicht zu sehen, woher eine Funktion mit dieser Signatur kam, aber es gibt nie eine solche Funktion, es ist nur eine bequeme Syntax, um Muster mit den Typen abzugleichen. Klug.
Mattnewport