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
und 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 ++x
nicht gültig ist, kann die requires_
Member-Funktion nicht aufgerufen werden. So können wir ein models
Merkmal 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 increment
Funktion definieren , die basierend auf dem Incrementable
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 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::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 dem std::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
{
template<class... Ts>
auto operator()(Ts&&... xs) -> decltype(F1()(std::forward<Ts>(xs)...))
{
return F1()(std::forward<Ts>(xs)...);
}
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, um basic_conditional
zu 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_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 ist constexpr
:
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 die decltype
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.