Infix-Funktionsaufrufe mit Boost HOF

Infix-Funktionsaufrufe mit Boost HOF

In C++ werden Funktionen mit einem Präfix aufgerufen Syntax. Das bedeutet, dass auf der Aufrufseite der Funktionsname vor den Parametern steht:

myFunction(parameter1, parameter2);
^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^
 function         parameters

Andererseits binäre Operatoren wie operator+ werden mit einem Infix aufgerufen Syntax, was bedeutet, dass der Operator zwischen den Parametern steht:

parameter1 + parameter2

Einige Sprachen erlauben es auch, Funktionen mit einer Infix-Syntax aufzurufen. Zum Beispiel erlaubt Haskell, eine Präfixfunktion in eine Infixfunktion umzuwandeln, indem Backticks verwendet werden:

parameter1 `myFunction` parameter2

C++ erlaubt das nicht.

Aber Boost sprengt wie so oft die Grenzen der Sprache, und mit der jüngsten HOF-Bibliothek ist es jetzt (unter anderem) möglich, die Infix-Notation in C++ zu emulieren.

Warum die Infix-Notation

Bevor wir uns mit der Implementierung befassen, was ist der Sinn einer Infix-Notation?

Die Infix-Notation kann den Code aussagekräftiger und korrekter machen.

Wenn zum Beispiel eine Funktion zwei Parameter des gleichen Typs verwendet, müssen wir die Rolle jedes einzelnen identifizieren. Nehmen wir das Beispiel einer Funktion, die einen Teilstring in einem String durchsucht. Der Standardalgorithmus search tut dies, und eine vereinfachte Version seiner C++20-Range-Schnittstelle sieht so aus:

template<forward_range Range1, forward_range Range2>
safe_subrange_t<Range1> search(Range1&& range1, Range2&& range2);

Da es einen Teilbereich des ersten Bereichs zurückgibt, können wir davon ausgehen, dass es nach range2 sucht in range1 . Aber schauen Sie sich die Aufrufseite an:

auto result = std::ranges::search(s1, s2);

Es ist nicht klar, welche Saite wir suchen und welche wir untersuchen. Und wenn es nicht klar ist, dann ist der Code nicht aussagekräftig und es besteht die Gefahr, dass die Parameter verwechselt werden, was zu einem Fehler führt.

Eine Möglichkeit, dem abzuhelfen, besteht darin, starke Typen zu verwenden, um die Rolle der Parameter auf der Aufrufseite zu identifizieren:

auto results = search(s2, Within(s1));

Oder manchmal mit originelleren Namen:

auto result = search(Needle(s2), Haystack(s1));

Aber wäre es nicht einfacher, so etwas zu schreiben:

auto result = s2 `searchedInto` s1; // imaginary C++

Ein weiteres Beispiel ist eine Funktion, die feststellt, ob ein String ein Präfix eines anderen ist:

auto result = isPrefixOf(s1, s2);

Es ist unklar, welche Zeichenfolge wir prüfen, das Präfix der anderen ist, und dies kann zu einem Fehler führen, wenn wir die Argumente verwechseln.

Es wäre so viel klarer, hier eine Infix-Notation zu verwenden:

auto result = s1 `isPrefixOf` s2; // imaginary C++

Sehen wir uns nun an, wie Boost HOF die Infix-Notation in C++ emuliert.

Die Infix-Notation mit Boost HOF

Boost HOF (steht für Higher Order Functions) ermöglicht es, die Infix-Notation mit jeder Funktion zu verwenden, die zwei Parameter benötigt, indem eine geschickte Überladung von operator< verwendet wird und operator> :Schließen Sie den Namen der Funktion in spitze Klammern ein, und die Bibliothek kümmert sich um den Rest.

Sehen wir uns an einem Beispiel an, wie es funktioniert, mit der Funktion, die überprüft, ob ein String ein Präfix eines anderen ist.

Wie wir im Artikel zur Prüfung auf Präfixe in C++ gesehen haben, ist hier eine sehr einfache Implementierung der Funktion:

bool isPrefixOf(std::string const& prefix, std::string const& text)
{
    auto const differingPositions = std::mismatch(begin(prefix), end(prefix), begin(text), end(text));
    return differingPositions.first == end(prefix);
}

Damit Boost HOF damit als Infix-Funktion arbeiten kann, verwenden wir boost::hof::infix :

#include <algorithm>
#include <string>
#include <boost/hof.hpp>

auto isPrefixOf = boost::hof::infix(
    [](std::string const& prefix, std::string const& text)
    {
        auto const differingPositions = std::mismatch(begin(prefix), end(prefix), begin(text), end(text));
        return differingPositions.first == end(prefix);
    });

Jetzt können wir einfach die Infix-Notation verwenden:

auto const result = s1 <isPrefixOf> s2;

Wie schön ist das?

Die Implementierung der Infix-Notation

Boost-Infix verwendet Operatorüberladung für operator< und operator> um die Infix-Notation in C++ zu implementieren.

Lassen Sie uns verstehen, wie dies implementiert wird. Diese Untersuchung ist an sich interessant, und durch das Verständnis der Implementierung werden wir auch die Fälle verstehen, in denen sie gut funktioniert, und die Fälle, die sie nicht unterstützt.

Versuchen wir, eine einfache Version von infix zu codieren .

Der infix eingeben

Im Wesentlichen die infix -Funktion erstellt ein Objekt, das die Vergleichsoperatoren überlädt. Es wird mit operator< kombiniert wobei das linke Argument ein Objekt erzeugt, das mit operator> kombiniert wird mit dem rechten Argument, Aufruf der Funktion für diese beiden Argumente.

Rufen Sie infix an mit einer Funktion gibt ein Objekt zurück, das diese Funktion speichert Mit der C++17-Ableitung von Vorlagenparametern in Konstruktoren können wir infix definieren als Typ dieses Objekts:

template<typename Function>
struct infix
{
    explicit infix(Function function) : function_(function){}
    Function function_;
};

Speichern des ersten Arguments

In Kombination mit dem ersten Argument infix muss ein Objekt zurückgeben, das später mit dem zweiten Argument kombiniert werden kann. Dieses Objekt muss auch die Funktion und auch den ersten Parameter speichern, um später den Funktionsaufruf durchzuführen. Nennen wir den Typ dieses Objekts LeftHandAndFunction :

template<typename LeftHandValue, typename Function>
struct LeftHandAndFunction
{
    LeftHandAndFunction(LeftHandValue const& leftHandValue, Function function) : leftHandValue_(leftHandValue), function_(function){}

    LeftHandValue leftHandValue_;
    Function function_;
};

In dieser Implementierung müssen wir entscheiden, wie der erste Parameter gespeichert werden soll. Speichern wir sie nach Wert oder nach Referenz?

Das Speichern nach Wert führt zu einer Verschiebung (oder Kopie) und trennt den übergebenen Wert von dem Wert, den die Funktion empfängt. Andererseits ist das Speichern per Referenz kompliziert zu implementieren:Wenn es sich um eine lvalue-Referenz handelt, muss es const sein , andernfalls wird es nicht an Rvalues ​​gebunden. Und wenn es nicht const ist , dann müssten wir nur in diesem Fall nach Wert speichern, um rvalues ​​unterzubringen.

Um mit einer einfachen Implementierung zu beginnen, speichern wir dieses erste Argument in allen Fällen als Wert und kopieren es aus der Eingabe. Das ist suboptimal, und wir werden gleich darauf zurückkommen.

operator< kombiniert dann infix Objekt mit dem ersten Argument:

template<typename LeftHandValue, typename Function>
LeftHandAndFunction<std::remove_reference_t<LeftHandValue>, Function> operator< (LeftHandValue&& leftHandValue, infix<Function> const& infix)
{
    return LeftHandAndFunction<std::remove_reference_t<LeftHandValue>, Function>(std::forward<LeftHandValue>(leftHandValue), infix.function_);
}

Wir verwenden std::remove_reference_t im Fall LeftHandValue ist eine Lvalue-Referenz. Auf diese Weise speichern wir den Wert des ersten Arguments und keinen Verweis darauf.

Speichern des ersten Arguments

Der nächste Schritt besteht darin, dieses Objekt mit dem zweiten Argument mit operator> zu kombinieren , wodurch die zum Aufrufen der Funktion erforderlichen Elemente vervollständigt werden:

template<typename LeftHandValue, typename Function, typename RightHandValue>
decltype(auto) operator> (LeftHandAndFunction<LeftHandValue, Function> leftHandAndFunction, RightHandValue&& rightHandValue)
{
    return leftHandAndFunction.function_(leftHandAndFunction.leftHandValue_, std::forward<RightHandValue>(rightHandValue));
}

Und das war es auch schon für eine Implementierung von infix Arbeiten in einfachen Fällen.

Umgang mit fortgeschritteneren Fällen

Nachdem wir nun die gesamte Struktur festgelegt haben, kommen wir darauf zurück, wie das erste Argument effizient gespeichert werden kann.

Der Code von Boost HOF speichert einen Verweis auf das erste Argument, wenn es ein lvalue ist, und verschiebt (oder kopiert) es hinein, wenn es ein rvalue ist. Um dies zu tun, verwendet es Techniken, die denen von Miguel ähneln, die uns gezeigt haben, wie man C++-Objekte konstruiert, ohne Kopien zu erstellen:

template<typename LeftHandValue, typename Function>
struct LeftHandAndFunction
{
    template<typename LeftHandValue_>
    LeftHandAndFunction(LeftHandValue_&& leftHandValue, Function function) : leftHandValue_(std::forward<LeftHandValue_>(leftHandValue)), function_(function){}

    LeftHandValue leftHandValue_;
    Function function_;
};

Beachten Sie, dass wir den Konstruktor zu einer Template-Funktion innerhalb einer Template-Klasse gemacht haben. Der Sinn der Verwendung eines neuen Template-Parameters (LeftHandValue_ , mit abschließendem Unterstrich), erlaubt die Verwendung von Weiterleitungsreferenzen. In der Tat aus der Sicht des Konstruktors LeftHandValue (ohne Unterstrich) ist kein Vorlagenparameter. Es wurde bei der Instanziierung des Codes der Klasse behoben.

Der Code von operator< sieht dann so aus:

template<typename LeftHandValue, typename Function>
LeftHandAndFunction<LeftHandValue, Function> operator< (LeftHandValue&& leftHandValue, infix<Function> const& infix)
{
    return LeftHandAndFunction<LeftHandValue, Function>(std::forward<LeftHandValue>(leftHandValue), infix.function_);
}

Beachten Sie, dass der std::remove_reference_t sind weg.

Wie funktioniert das alles?

Wenn der erste Parameter ein Lvalue ist, dann LeftHandValue ist eine Lvalue-Referenz und LeftHandAndFunction speichert eine Referenz (die auch nicht const sein kann ) zum ersten Parameter.

Wenn der erste Parameter ein rvalue ist, der LeftHandValue ist eine weitere Instanz des Werts des ersten Arguments selbst. Bringen Sie diesen Anfangswert mit std::forward ein trägt die Information, dass es von einem Rvalue stammt. Daher der Wert in LeftHandAndFunction wird mit einem Zug gefüllt, wenn er auf dem Typ verfügbar ist (und andernfalls mit einer Kopie).

Und was ist, wenn das erste Argument nicht verschoben oder kopiert werden kann, zum Beispiel wenn es um unique_ptr geht als lvalues ​​übergeben? In diesem Fall würde der Code auch mit Boost HOF nicht kompilieren, wie wir an diesem Beispiel sehen können.

Funktionen höherer Ordnung

Mit diesem schönen infix Helfer, der uns mehr Flexibilität gibt, um aussagekräftigen und korrekten Code zu schreiben, sieht Boost HOF wie eine sehr interessante Bibliothek aus.

Wir werden in zukünftigen Beiträgen mehr seiner Komponenten untersuchen.