Vor einiger Zeit habe ich einen Artikel gelesen, der einige Fallstricke der argumentabhängigen Suche erklärt hat, aber ich kann ihn nicht mehr finden. Es ging darum, Zugang zu Dingen zu bekommen, zu denen man keinen Zugang haben sollte oder so etwas. Also dachte ich, ich würde hier fragen: Was sind die Fallstricke von ADL?
Was sind die Fallstricke von ADL?
fredoverflow
James McNellis
Es gibt ein großes Problem mit der argumentabhängigen Suche. Betrachten Sie beispielsweise das folgende Dienstprogramm:
#include <iostream>
namespace utility
{
template <typename T>
void print(T x)
{
std::cout << x << std::endl;
}
template <typename T>
void print_n(T x, unsigned n)
{
for (unsigned i = 0; i < n; ++i)
print(x);
}
}
Es ist einfach genug, oder? Wir können anrufen print_n()
und übergeben Sie ihm ein beliebiges Objekt und es wird aufgerufen print
um das Objekt zu drucken n
mal.
Tatsächlich stellt sich heraus, dass, wenn wir uns nur diesen Code ansehen, wir haben absolut keine ahnung welche Funktion aufgerufen wird print_n
. Es könnte die sein print
Funktionsvorlage, die hier angegeben ist, aber möglicherweise nicht. Wieso den? Argumentabhängige Suche.
Nehmen wir als Beispiel an, Sie haben eine Klasse geschrieben, um ein Einhorn darzustellen. Aus irgendeinem Grund haben Sie auch eine Funktion namens definiert print
(was für ein Zufall!), der das Programm nur zum Absturz bringt, indem er auf einen dereferenzierten Nullzeiger schreibt (wer weiß, warum Sie das getan haben; das ist nicht wichtig):
namespace my_stuff
{
struct unicorn { /* unicorn stuff goes here */ };
std::ostream& operator<<(std::ostream& os, unicorn x) { return os; }
// Don't ever call this! It just crashes! I don't know why I wrote it!
void print(unicorn) { *(int*)0 = 42; }
}
Als nächstes schreiben Sie ein kleines Programm, das ein Einhorn erstellt und es viermal druckt:
int main()
{
my_stuff::unicorn x;
utility::print_n(x, 4);
}
Sie kompilieren dieses Programm, führen es aus und… es stürzt ab. “Was?! Auf keinen Fall”, sagst du: “Ich habe nur angerufen print_n
die die ruft print
Funktion, um das Einhorn viermal zu drucken!” Ja, das stimmt, aber es hat nicht aufgerufen print
Funktion, von der Sie erwartet haben, dass sie aufgerufen wird. Es heißt my_stuff::print
.
Warum ist my_stuff::print
ausgewählt? Während der Namenssuche sieht der Compiler, dass das Argument für den Aufruf to print
ist vom Typ unicorn
bei dem es sich um einen Klassentyp handelt, der im Namespace deklariert wird my_stuff
.
Aufgrund der argumentabhängigen Suche schließt der Compiler diesen Namespace in seine Suche nach benannten Kandidatenfunktionen ein print
. Es findet my_stuff::print
der dann während der Überlastungsauflösung als der beste brauchbare Kandidat ausgewählt wird: Es ist keine Konvertierung erforderlich, um einen der Kandidaten aufzurufen print
Funktionen und Nicht-Template-Funktionen werden gegenüber Funktions-Templates bevorzugt, also die Non-Template-Funktion my_stuff::print
ist die beste Übereinstimmung.
(Wenn Sie das nicht glauben, können Sie den Code in dieser Frage unverändert kompilieren und ADL in Aktion sehen.)
Ja, die argumentabhängige Suche ist ein wichtiges Feature von C++. Es ist im Wesentlichen erforderlich, um das gewünschte Verhalten einiger Sprachfunktionen wie überladener Operatoren zu erreichen (beachten Sie die Streams-Bibliothek). Allerdings ist es auch sehr, sehr fehlerhaft und kann zu wirklich hässlichen Problemen führen. Es gab mehrere Vorschläge, argumentabhängige Suche zu reparieren, aber keiner davon wurde vom C++-Standardkomitee akzeptiert.
-
Ist dies ein Fallstrick von ADL oder ein Fallstrick, wenn Sie ADL nicht sorgfältig verwenden?
– Chubsdad
22. November 10 um 5:33 Uhr
-
@Chubsdad: Es ist eine große Falle von ADL. Das Problem ist, dass Sie zwei Bibliotheken schreiben können, die völlig unabhängig sind und versehentlich auf dieses Problem stoßen, ohne zu ahnen, dass Sie Probleme haben werden. Keine noch so große „Sorgfalt“ kann Sie vollständig davor schützen.
– James McNellis
22. November 10 um 5:36 Uhr
-
@MSalters: Nun, das Problem ist, dass die explizite Anweisung, die die Bibliotheken mischt, nicht immer so explizit ist. Betrachten Sie zB, wenn Sie eine Namensraumbereichsfunktionsschablone namens schreiben
merge
das verschmilzt zwei Dinge und du übergibst zwei es zweistd::vector
Objekte. Sie erhalten unterschiedliche Ergebnisse, je nachdem, ob Sie eingeschlossen haben<algorithm>
(was erklärtstd::merge
).– James McNellis
4. Februar 11 um 15:35 Uhr
-
Ich würde sagen, dies ist kein Fallstrick, sondern ein Feature: Es ermöglicht Ihnen, das Verhalten einer Bibliothek zu überschreiben, indem Sie eine Implementierung bereitstellen, die auf Ihre Typen spezialisiert ist. Ohne ADL wären Sie nicht in der Lage, Änderungen vorzunehmen
print
‘s Verhalten, um Ihrem entgegenzukommenunicorn
Typ. Eine weit verbreitete Anwendung davon istswap
: Viele Standardalgorithmen müssen Werte austauschen; Sie können Ihre eigene optimierte Version von bereitstellenswpa
und es wird dank ADL ausgewählt. Natürlich wäre es besser, wenn Sie dieses Überschreiben verhindern könnten, wenn es nicht erwünscht ist (genauso wie Sie nicht verpflichtet sind, Ihre Mitgliederfunktionen virtuell zu machen).– Luc Touraille
1. Dezember 11 um 15:26 Uhr
-
Die Frage ist, warum würde der Programmierer schreiben
print(x)
wenn er anrufen will::utility::print()
? Wenn ich schreibeprint(x)
dann ich beabsichtigen um ADL aufzurufen, um zu finden die richtige Überladung (evtl. auch in anderen Namespaces). Wenn ich kein ADL will, dann würde ich schreiben::utility::print(x)
. Also ich nicht völlig stimme dieser Antwort zu. Es entsteht meist aus Mangel an Basic Kenntnisse über ADL. Ich würde stattdessen @LucTouraille zustimmen. 🙂– Nawaz
9. Oktober 13 um 6:56 Uhr
FrankHB
Die akzeptierte Antwort ist einfach falsch – dies ist kein Fehler von ADL. Es zeigt ein sorgloses Antimuster für die Verwendung von Funktionsaufrufen in der täglichen Codierung – Unkenntnis abhängiger Namen und blindes Vertrauen auf unqualifizierte Funktionsnamen.
Kurz gesagt, wenn Sie einen nicht qualifizierten Namen in der verwenden postfix-expression
eines Funktionsaufrufs, Sie sollen haben bestätigt, dass Sie die Möglichkeit gegeben haben, dass die Funktion an anderer Stelle “überschrieben” werden kann (ja, dies ist eine Art statischer Polymorphismus). Somit ist die Schreibweise des unqualifizierten Namens einer Funktion in C++ genau ein Teil der Schnittstelle.
Im Falle der akzeptierten Antwort, wenn die print_n
wirklich brauchen ADL print
(dh es zuzulassen, dass es außer Kraft gesetzt wird), sollte es mit der Verwendung von unqualifiziert dokumentiert worden sein print
als ausdrücklicher Hinweis, damit Kunden einen Vertrag erhalten würden print
sollten sorgfältig deklariert werden, und das Fehlverhalten würde vollständig in der Verantwortung von liegen my_stuff
. Andernfalls ist es ein Fehler von print_n
. Die Lösung ist einfach: sich qualifizieren print
mit Präfix utility::
. Dies ist in der Tat ein Fehler von print_n
aber kaum ein Fehler der ADL-Regeln in der Sprache.
Allerdings dort tun Unerwünschte Dinge existieren in der Sprachspezifikation und technisch gesehen nicht nur einer. Sie werden seit mehr als 10 Jahren realisiert, aber noch ist nichts in der Sprache festgelegt. Sie werden von der akzeptierten Antwort übersehen (außer dass der letzte Absatz bisher nur richtig ist). Sieh dir das an Papier für Details.
Ich kann einen echten Fall gegen die böse Namenssuche anhängen. Ich implementierte is_nothrow_swappable
wo __cplusplus < 201703L
. Ich fand es unmöglich, mich auf ADL zu verlassen, um eine solche Funktion zu implementieren, sobald ich eine deklariert habe swap
Funktionsvorlage in meinem Namensraum. Eine solche swap
würde immer zusammen mit gefunden std::swap
eingeleitet durch ein Idiomatik using std::swap;
ADL gemäß den ADL-Regeln zu verwenden, und dann würde es zu Mehrdeutigkeiten kommen swap
bei dem die swap
Vorlage (die instantiieren würde is_nothrow_swappable
das Richtige zu bekommen noexcept-specification
) wird genannt. In Kombination mit 2-Phasen-Lookup-Regeln zählt die Reihenfolge der Deklarationen nicht, sobald der Bibliotheksheader die enthält swap
Vorlage ist enthalten. Also, wenn ich nicht überlaste alle meine Bibliothekstypen mit spezialisiert swap
-Funktion (um alle möglichen generischen Vorlagen zu unterdrücken swap
durch Überladen der Auflösung nach ADL abgeglichen), kann ich die Vorlage nicht deklarieren. Ironischerweise die swap
Die in meinem Namensraum deklarierte Vorlage dient genau der Verwendung von ADL (betrachten Sie boost::swap
) und ist einer der bedeutendsten Direktkunden von is_nothrow_swappable
in meiner Bibliothek (Übrigens, boost::swap
respektiert die Ausnahmespezifikation nicht). Das hat meinen Zweck perfekt verfehlt, seufz…
#include <type_traits>
#include <utility>
#include <memory>
#include <iterator>
namespace my
{
#define USE_MY_SWAP_TEMPLATE true
#define HEY_I_HAVE_SWAP_IN_MY_LIBRARY_EVERYWHERE false
namespace details
{
using ::std::swap;
template<typename T>
struct is_nothrow_swappable
: std::integral_constant<bool, noexcept(swap(::std::declval<T&>(), ::std::declval<T&>()))>
{};
} // namespace details
using details::is_nothrow_swappable;
#if USE_MY_SWAP_TEMPLATE
template<typename T>
void
swap(T& x, T& y) noexcept(is_nothrow_swappable<T>::value)
{
// XXX: Nasty but clever hack?
std::iter_swap(std::addressof(x), std::addressof(y));
}
#endif
class C
{};
// Why I declared 'swap' above if I can accept to declare 'swap' for EVERY type in my library?
#if !USE_MY_SWAP_TEMPLATE || HEY_I_HAVE_SWAP_IN_MY_LIBRARY_EVERYWHERE
void
swap(C&, C&) noexcept
{}
#endif
} // namespace my
int
main()
{
my::C a, b;
#if USE_MY_SWAP_TEMPLATE
my::swap(a, b); // Even no ADL here...
#else
using std::swap; // This merely works, but repeating this EVERYWHERE is not attractive at all... and error-prone.
swap(a, b); // ADL rocks?
#endif
}
Versuchen https://wandbox.org/permlink/4pcqdx0yYnhhrASi und umdrehen USE_MY_SWAP_TEMPLATE
zu true
um die Mehrdeutigkeit zu sehen.
Aktualisierung 05.11.2018:
Aha, ich werde heute morgen wieder von ADL gebissen. Diesmal hat es sogar nichts mit Funktionsaufrufen zu tun!
Heute beende ich die Arbeit der Portierung ISO C++17 std::polymorphic_allocator
zu meiner Codebasis. Da einige Container-Klassen-Vorlagen vor langer Zeit in meinen Code eingeführt wurden (wie z Dies), dieses Mal ersetze ich nur die Deklarationen durch Alias-Vorlagen wie:
namespace pmr = ystdex::pmr;
template<typename _tKey, typename _tMapped, typename _fComp
= ystdex::less<_tKey>, class _tAlloc
= pmr::polymorphic_allocator<std::pair<const _tKey, _tMapped>>>
using multimap = std::multimap<_tKey, _tMapped, _fComp, _tAlloc>;
… damit es verwendet werden kann meine Implementierung von polymorphic_allocator
standardmäßig. (Haftungsausschluss: Es hat einige bekannte Fehler. Korrekturen der Fehler würden in ein paar Tagen durchgeführt.)
Aber es funktioniert plötzlich nicht mehr, mit Hunderten von Zeilen kryptischer Fehlermeldungen…
Der Fehler beginnt ab diese Linie. Es beschwert sich grob, dass die erklärt BaseType
ist keine Basis der einschließenden Klasse MessageQueue
. Das erscheint sehr seltsam, da der Alias mit genau den gleichen Token deklariert ist wie die in der base-specifier-list der Klassendefinition, und ich bin mir sicher, dass nichts davon makroerweitert werden kann. Warum also?
Die Antwort ist… ADL ist scheiße. Die Linie einführend BaseType
ist fest codiert mit a std
name als Template-Argument, sodass das Template nach ADL-Regeln gesucht wird im Klassenbereich. So findet es std::multimap
die sich vom Ergebnis der Suche insofern unterscheidet, als die tatsächlich deklarierte Basisklasse im einschließenden Namespace-Bereich. Seit std::multimap
Verwendet std::allocator
Instanz als Standard-Template-Argument, BaseType
ist nicht derselbe Typ wie die eigentliche Basisklasse, die eine Instanz von hat polymorphic_allocator
auch multimap
der im einschließenden Namensraum deklariert ist, wird umgeleitet std::multimap
. Durch Hinzufügen der umschließenden Qualifikation als Präfix rechts zum =
der Fehler ist behoben.
Ich gebe zu, ich habe genug Glück. Die Fehlermeldungen führen das Problem in diese Zeile. Es gibt nur 2 ähnliche Probleme u das andere ist ohne explizite std
(wo string
ist meine eigene an ISO C++17 angepasst string_view
ändern, nicht std
eine im Pre-C++17-Modus). Ich würde nicht so schnell herausfinden, dass es sich bei dem Fehler um ADL handelt.
-
Ich denke, die meisten Menschen sind sich heutzutage einig, dass die ADL-Regeln, wie sie definiert wurden, ein Fehler waren (nicht ADL an sich). Es ist eine lästige Pflicht, alle Funktionsaufrufe für Symbole in Ihrem eigenen Namensraum zu qualifizieren (was im Anwendungscode die meisten davon sind). Es schadet auch der Lesbarkeit. Die Vorgabe hätte das Gegenteil sein sollen: explizit markieren, welche Aufrufe ADL ausführen sollen.
– Eichel
4. November 18 um 20:17 Uhr
-
@Acorn Das ist vielleicht besser im Sinne von POLA, aber wenn dies wahr gewesen wäre, hätte es vermutlich eine Unterscheidungssyntax gegeben, die speziell für ADL entwickelt wurde. Es könnte jedoch andere Möglichkeiten geben. Wie auch immer, ADL setzt die “normalen” Regeln für die Namenssuche während der Übersetzung außer Kraft, also warum nicht einige allgemeinere Metaprogrammierungsfunktionen, die es ermöglichen, dass es programmierbar ist (z hygienische Makros)? Aber leider wurde dies bei der Gestaltung nicht berücksichtigt.
– FrankHB
5. November 18 um 0:21 Uhr
.
Großartiger relevanter Blogbeitrag von Arthur O’Dwyer zu Mehrdeutigkeitsproblemen bei Aufrufen von
size
wegen ADL wiestd::size
wurde in C++17 hinzugefügtsehen: https://quuxplusone.github.io/blog/2018/06/17/std-size.– Amir Kirsch
7. Februar 21 um 13:35 Uhr