
Einführung
Wir haben wiederholt nach Fehlern in Projekten gesucht, die maschinelles Lernen verwenden, und DeepSpeech war für uns keine Ausnahme. Kein Wunder, denn dieses Projekt ist sehr beliebt: Zum Zeitpunkt dieses Schreibens hat es bereits mehr als 15.000 Sterne auf GitHub.
Wie üblich wurde die Suche nach Fehlern, die ich in diesem Artikel zitieren werde, mit dem statischen Codeanalysator PVS-Studio durchgeführt.
DeepSpeech verwendet für seine Arbeit die TensorFlow-Bibliothek. Ich habe die Analyse des Codes dieser Bibliothek deaktiviert, da wir bereits einen separaten Artikel darüber geschrieben haben.Ich habe jedoch die Analyse der übrigen verwendeten Bibliotheken nicht deaktiviert. Was ist der Grund dafür? Fehler in einer Bibliothek, die Sie in Ihr Projekt aufnehmen, werden auch zu Fehlern in Ihrem Projekt. Daher ist es hilfreich, nicht nur Ihren, sondern auch den von Ihnen verwendeten Code von Drittanbietern zu analysieren. Eine ausführliche Stellungnahme dazu finden Sie in unserem aktuellen Artikel .
Damit ist die kurze Einführung abgeschlossen - es ist Zeit, mit der Fehleranalyse fortzufahren. Übrigens, wenn Sie hierher gekommen sind, um die Antwort auf die Frage herauszufinden, die ich im Titel des Artikels gestellt habe (warum Sie nicht in den Namespace std schreiben sollten), können Sie sich sofort das Ende des Artikels ansehen. Dort erwartet Sie ein besonders interessantes Beispiel!
Übersicht über 10 interessante Warnungen des Analysators
Warnung 1
V773 Die Funktion wurde beendet, ohne den ' Daten' -Zeiger loszulassen . Ein Speicherverlust ist möglich. edit-fst.h 311
// EditFstData method implementations: just the Read method.
template <typename A, typename WrappedFstT, typename MutableFstT>
EditFstData<A, WrappedFstT, MutableFstT> *
EditFstData<A, WrappedFstT, MutableFstT>::Read(std::istream &strm,
const FstReadOptions &opts)
{
auto *data = new EditFstData<A, WrappedFstT, MutableFstT>();
// next read in MutabelFstT machine that stores edits
FstReadOptions edits_opts(opts);
....
std::unique_ptr<MutableFstT> edits(MutableFstT::Read(strm, edits_opts));
if (!edits) return nullptr; // <=
....
}
Dieses Code-Snippet enthält ein klassisches Beispiel für einen Speicherverlust: Die Read- Funktion ruft ' return nullptr ' auf, ohne den mit dem Ausdruck ' new EditFstData ' zugewiesenen Speicher freizugeben . Bei einem solchen Verlassen der Funktion (ohne Aufruf von delete data ) wird nur der Zeiger selbst gelöscht, und der Destruktor des Objekts, auf das er zeigt, wird nicht aufgerufen. Somit wird das Objekt weiterhin im Speicher gespeichert und es ist nicht mehr möglich, es zu löschen oder zu verwenden.
Neben einem Fehler enthält dieser Code auch eine andere nicht sehr gute Vorgehensweise: Der Code einer Funktion verwendet gleichzeitig sowohl intelligente als auch gewöhnliche Zeiger. Zum Beispiel, wenn Datenwar auch ein intelligenter Zeiger, dann würde ein solcher Fehler nicht auftreten: Falls erforderlich, rufen intelligente Zeiger beim Verlassen des Gültigkeitsbereichs automatisch den Destruktor des gespeicherten Objekts auf.
Warnung 2
V1062 Die Klasse 'DfsState' definiert einen benutzerdefinierten 'neuen' Operator. Der Operator 'Löschen' muss ebenfalls definiert werden. dfs-visit.h 62
// An FST state's DFS stack state.
template <class FST>
struct DfsState {
public:
....
void *operator new(size_t size,
MemoryPool<DfsState<FST>> *pool) {
return pool->Allocate();
}
....
}
PVS-Studio steht nicht still und fügt weiterhin neue Diagnosen hinzu. Dieses Code-Snippet ist ein hervorragendes Beispiel für die Arbeit der neuesten Diagnose mit der Nummer V1062 .
Die Regel, die diese Diagnose überwacht, ist einfach: Wenn Sie Ihren eigenen 'neuen' Operator definieren, müssen Sie auch Ihren eigenen 'Lösch'-Operator definieren. Das Gegenteil funktioniert genauso: Wenn Sie Ihr eigenes "Löschen" definieren, muss auch Ihr eigenes "Neues" definiert werden.
Im obigen Beispiel wird gegen diese Regel verstoßen: Das Objekt wird mit dem von uns definierten "Neu" erstellt und mit dem Standard "Löschen" gelöscht. Mal sehen, was die Allocate- Funktion der MemoryPool- Klasse bewirkt :was unser eigenes "neues" nennt:
void *Allocate() {
if (free_list_ == nullptr) {
auto *link = static_cast<Link *>(mem_arena_.Allocate(1));
link->next = nullptr;
return link;
} else {
auto *link = free_list_;
free_list_ = link->next;
return link;
}
}
Diese Funktion erstellt ein Element und fügt es der verknüpften Liste hinzu. Es ist logisch, dass eine solche Zuordnung in einem eigenen "neuen" Format geschrieben worden sein sollte.
Aber warte mal! Nur ein paar Zeilen weiter unten enthält die folgende Funktion:
void Free(void *ptr) {
if (ptr) {
auto *link = static_cast<Link *>(ptr);
link->next = free_list_;
free_list_ = link;
}
}
Dies bedeutet, dass wir bereits vorgefertigte Funktionen für die Zuordnung und Freigabe haben. Höchstwahrscheinlich musste der Programmierer seinen eigenen 'Lösch'-Operator schreiben und die Free () -Funktion verwenden, um ihn freizugeben .
Der Analysator hat mindestens drei weitere solche Fehler festgestellt:
- V1062 Die Klasse 'VectorState' definiert einen benutzerdefinierten 'neuen' Operator. Der Operator 'Löschen' muss ebenfalls definiert werden. vector-fst.h 31
- V1062 Die Klasse 'CacheState' definiert einen benutzerdefinierten 'neuen' Operator. Der Operator 'Löschen' muss ebenfalls definiert werden. cache.h 65
Warnung 3
V703 Es ist seltsam, dass das Feld 'first_path' in der abgeleiteten Klasse 'ShortestPathOptions' das Feld in der Basisklasse 'ShortestDistanceOptions' überschreibt. Kontrolllinien: kürzester Weg.h: 35, kürzester Weg.h: 34. kürzester Weg.h 35
// Base class
template <class Arc, class Queue, class ArcFilter>
struct ShortestDistanceOptions {
Queue *state_queue; // Queue discipline used; owned by caller.
ArcFilter arc_filter; // Arc filter (e.g., limit to only epsilon graph).
StateId source; // If kNoStateId, use the FST's initial state.
float delta; // Determines the degree of convergence required
bool first_path; // For a semiring with the path property (o.w.
// undefined), compute the shortest-distances along
// along the first path to a final state found
// by the algorithm. That path is the shortest-path
// only if the FST has a unique final state (or all
// the final states have the same final weight), the
// queue discipline is shortest-first and all the
// weights in the FST are between One() and Zero()
// according to NaturalLess.
ShortestDistanceOptions(Queue *state_queue, ArcFilter arc_filter,
StateId source = kNoStateId,
float delta = kShortestDelta)
: state_queue(state_queue),
arc_filter(arc_filter),
source(source),
delta(delta),
first_path(false) {}
};
// Derived class
template <class Arc, class Queue, class ArcFilter>
struct ShortestPathOptions
: public ShortestDistanceOptions<Arc, Queue, ArcFilter> {
using StateId = typename Arc::StateId;
using Weight = typename Arc::Weight;
int32 nshortest; // Returns n-shortest paths.
bool unique; // Only returns paths with distinct input strings.
bool has_distance; // Distance vector already contains the
// shortest distance from the initial state.
bool first_path; // Single shortest path stops after finding the first
// path to a final state; that path is the shortest path
// only when:
// (1) using the ShortestFirstQueue with all the weights
// in the FST being between One() and Zero() according to
// NaturalLess or when
// (2) using the NaturalAStarQueue with an admissible
// and consistent estimate.
Weight weight_threshold; // Pruning weight threshold.
StateId state_threshold; // Pruning state threshold.
ShortestPathOptions(Queue *queue, ArcFilter filter, int32 nshortest = 1,
bool unique = false, bool has_distance = false,
float delta = kShortestDelta, bool first_path = false,
Weight weight_threshold = Weight::Zero(),
StateId state_threshold = kNoStateId)
: ShortestDistanceOptions<Arc, Queue, ArcFilter>(queue, filter,
kNoStateId, delta),
nshortest(nshortest),
unique(unique),
has_distance(has_distance),
first_path(first_path),
weight_threshold(std::move(weight_threshold)),
state_threshold(state_threshold) {}
};
Stimmen Sie zu, es ist nicht so einfach, einen möglichen Fehler zu finden, oder?
Das Problem ist, dass sowohl die Basis- als auch die abgeleitete Klasse Felder mit demselben Namen enthalten: first_path . Dies führt dazu, dass die abgeleitete Klasse ein eigenes, anderes Feld hat, das das Feld der Basisklasse mit seinem Namen überschreibt. Solche Fehler können zu ernsthafter Verwirrung führen.
Um besser zu verstehen, was ich meine, schlage ich vor, ein kurzes synthetisches Beispiel aus unserer Dokumentation zu betrachten. Angenommen, wir haben den folgenden Code:
class U {
public:
int x;
};
class V : public U {
public:
int x; // <= V703 here
int z;
};
Hier wird der Name x innerhalb der abgeleiteten Klasse überschrieben. Die Frage ist nun: Welchen Wert wird der folgende Code drucken?
int main() {
V vClass;
vClass.x = 1;
U *uClassPtr = &vClass;
std::cout << uClassPtr->x << std::endl;
....
}
Wenn Sie glauben, dass ein undefinierter Wert ausgegeben wird, haben Sie Recht. In diesem Beispiel wird die Einheit in das Feld der abgeleiteten Klasse geschrieben, aber das Lesen erfolgt aus dem Feld der Basisklasse, das zum Zeitpunkt der Ausgabe noch undefiniert ist.
Überlappende Namen in der Klassenhierarchie sind ein potenzieller Fehler und sollten vermieden werden :)
Warnung 4
V1004 Der Zeiger 'aiter' wurde unsicher verwendet, nachdem er gegen nullptr überprüft wurde. Überprüfen Sie die Zeilen: 107, 119. visit.h 119
template <....>
void Visit(....)
{
....
// Deletes arc iterator if done.
auto *aiter = arc_iterator[state];
if ((aiter && aiter->Done()) || !visit) {
Destroy(aiter, &aiter_pool);
arc_iterator[state] = nullptr;
state_status[state] |= kArcIterDone;
}
// Dequeues state and marks black if done.
if (state_status[state] & kArcIterDone) {
queue->Dequeue();
visitor->FinishState(state);
state_status[state] = kBlackState;
continue;
}
const auto &arc = aiter->Value(); // <=
....
}
Der Aiter- Zeiger wird verwendet, nachdem er auf nullptr überprüft wurde . Der Analysator geht davon aus, dass ein Zeiger, wenn er auf nullptr geprüft wird , während der Prüfung einen solchen Wert haben kann.
In diesem Fall wollen wir sehen, was mit aiter passiert, wenn es wirklich gleich Null ist. Zunächst wird dieser Zeiger in der Anweisung ' if ((aiter && aiter-> Done ()) ||! Visit) ' überprüft . Diese Bedingung wird falsch sein , und wir werden nicht in den damaligen Zweig dieses if gelangen . Und dann wird nach allen Kanonen klassischer Fehler ein Nullzeiger dereferenziert: ' aiter-> Value ();'. Diese Dereferenzierung führt zu undefiniertem Verhalten.
Warnung 5
Das folgende Beispiel enthält zwei Fehler gleichzeitig:
- V595 Der Zeiger 'istrm' wurde verwendet, bevor er gegen nullptr verifiziert wurde. Überprüfen Sie die Zeilen: 60, 61. mapped-file.cc 60
- V595 Der Zeiger 'istrm' wurde verwendet, bevor er gegen nullptr verifiziert wurde. Überprüfen Sie die Zeilen: 39, 61. mapped-file.cc 39
MappedFile *MappedFile::Map(std::istream *istrm, bool memorymap,
const string &source, size_t size) {
const auto spos = istrm->tellg(); // <=
....
istrm->seekg(pos + size, std::ios::beg); // <=
if (istrm) { // <=
VLOG(1) << "mmap'ed region of " << size
<< " at offset " << pos
<< " from " << source
<< " to addr " << map;
return mmf.release();
}
....
}
Der hier gefundene Fehler ist deutlicher als der Fehler aus dem vorherigen Beispiel. Der istrm- Zeiger wird zuerst (zweimal) dereferenziert, und danach folgt eine Nullprüfung und Fehlerprotokollierung. Dies zeigt deutlich: Wenn ein Nullzeiger als istrm auf diese Funktion kommt , tritt undefiniertes Verhalten (oder wahrscheinlicher ein Programmabsturz) ohne Protokollierung auf. Störung ... Fehler wie diese sind es nicht wert, übersehen zu werden.

Warnung 6
V730 Nicht alle Mitglieder einer Klasse werden im Konstruktor initialisiert. Betrachten Sie die Inspektion: stone_written_. ersatz_progress.cc 14
ErsatzProgress::ErsatzProgress()
: current_(0)
, next_(std::numeric_limits<uint64_t>::max())
, complete_(next_)
, out_(NULL)
{}
Der Analysator warnt uns, dass der Konstruktor nicht alle Felder der ErzatzProgress- Struktur initialisiert . Vergleichen wir diesen Konstruktor mit der Liste der Felder in dieser Struktur:
class ErsatzProgress {
....
private:
void Milestone();
uint64_t current_, next_, complete_;
unsigned char stones_written_;
std::ostream *out_;
};
In der Tat können Sie sehen, dass der Konstruktor alle Felder außer stone_written_ initialisiert .
Hinweis : Dieses Beispiel ist möglicherweise kein Fehler. Der eigentliche Fehler tritt nur auf, wenn der Wert eines nicht initialisierten Feldes verwendet wird .
Die V730- Diagnose hilft Ihnen jedoch, solche Anwendungsfälle im Voraus zu debuggen. Schließlich stellt sich natürlich die Frage: Wenn der Programmierer beschlossen hat, alle Felder der Klasse spezifisch zu initialisieren, warum sollte er dann einen Grund haben, ein Feld ohne Wert zu lassen?
Meine Vermutung, dass das Feld stone_written_ nicht versehentlich initialisiert wurde, wurde bestätigt, als ich einige Zeilen weiter unten einen anderen Konstruktor sah:
ErsatzProgress::ErsatzProgress(uint64_t complete,
std::ostream *to,
const std::string &message)
: current_(0)
, next_(complete / kWidth)
, complete_(complete)
, stones_written_(0)
, out_(to)
{
....
}
Hier werden alle Felder der Klasse initialisiert, was bestätigt: Der Programmierer hatte wirklich vor, alle Felder zu initialisieren, vergaß jedoch versehentlich eines.
Warnung 7
V780 Das Objekt '& params' eines nicht passiven (nicht PDS) Typs kann nicht mit der Memset-Funktion initialisiert werden. binary_format.cc 261
/* Not the best numbering system,
but it grew this way for historical reasons
* and I want to preserve existing binary files. */
typedef enum
{
PROBING=0,
REST_PROBING=1,
TRIE=2,
QUANT_TRIE=3,
ARRAY_TRIE=4,
QUANT_ARRAY_TRIE=5
}
ModelType;
....
struct FixedWidthParameters {
unsigned char order;
float probing_multiplier;
// What type of model is this?
ModelType model_type;
// Does the end of the file
// have the actual strings in the vocabulary?
bool has_vocabulary;
unsigned int search_version;
};
....
// Parameters stored in the header of a binary file.
struct Parameters {
FixedWidthParameters fixed;
std::vector<uint64_t> counts;
};
....
void BinaryFormat::FinishFile(....)
{
....
// header and vocab share the same mmap.
Parameters params = Parameters();
memset(¶ms, 0, sizeof(Parameters)); // <=
....
}
Um diese Warnung zu verstehen, empfehle ich zunächst zu verstehen, was ein PDS-Typ ist. PDS steht für Passive Data Structure, eine einfache Datenstruktur. Manchmal sagen sie anstelle von "PDS" "POD" - "Plain Old Data". In einfachen Worten (ich zitiere aus der russischen Wikipedia ) ist der PDS-Typ ein Datentyp, der eine fest definierte Position von Feldern im Speicher aufweist, für die keine Zugriffsbeschränkungen und keine automatische Steuerung erforderlich sind. Einfach ausgedrückt handelt es sich um einen Datentyp, der nur integrierte Typen enthält.
Eine Besonderheit von POD-Typen besteht darin, dass Variablen dieser Typen mithilfe primitiver Speicherverwaltungsfunktionen (memset, memcpy usw.) geändert und verarbeitet werden können. Dies kann jedoch nicht über "Nicht-PDS" -Typen gesagt werden: Eine derart niedrige Behandlung ihrer Werte kann zu schwerwiegenden Fehlern führen. Zum Beispiel Speicherlecks, doppeltes Leeren derselben Ressource oder undefiniertes Verhalten.
PVS-Studio gibt eine Warnung zu dem oben angegebenen Code aus: Sie können eine Struktur vom Typ Parameter nicht auf diese Weise behandeln. Wenn Sie sich die Definition dieser Struktur ansehen, können Sie sehen, dass ihr zweites Element vom Typ std :: vector ist... Dieser Typ verwendet aktiv die automatische Speicherverwaltung und speichert zusätzlich zu den Inhaltsdaten zusätzliche Dienstvariablen. Das Nullstellen eines solchen Feldes mit memset kann die Logik der Klasse brechen und ist ein schwerwiegender Fehler.
Warnung 8
V575 Der potenzielle Nullzeiger wird an die Funktion 'memcpy' übergeben. Überprüfen Sie das erste Argument. Überprüfen Sie die Zeilen: 73, 68.modelstate.cc 73
Metadata*
ModelState::decode_metadata(const DecoderState& state,
size_t num_results)
{
....
Metadata* ret = (Metadata*)malloc(sizeof(Metadata));
....
memcpy(ret, &metadata, sizeof(Metadata));
return ret;
}
Die nächste Warnung gibt an, dass ein Nullzeiger an die Funktion memcpy übergeben wird . Ja, wenn die malloc- Funktion keinen Speicher zuweist , wird NULL zurückgegeben . In diesem Fall wird dieser Zeiger an die Memset- Funktion übergeben , wo er dereferenziert wird - und dementsprechend ein bezaubernder Programmabsturz.
Einige unserer Leser sind jedoch möglicherweise empört: Wenn der Speicher so überfüllt / fragmentiert ist, dass malloc keinen Speicher zuweisen konnte, spielt es dann wirklich eine Rolle, was als nächstes passiert? Das Programm stürzt trotzdem ab, da es aufgrund von Speichermangel nicht normal funktionieren kann.
Wir sind wiederholt auf diese Meinung gestoßen und glauben, dass sie falsch ist. Ich würde Ihnen im Detail sagen, warum dies wirklich so ist, aber dieses Thema verdient einen separaten Artikel. So sehr verdient, dass wir es vor ein paar Jahren geschrieben haben :) Wenn Sie sich fragen, warum Sie immer einen Zeiger überprüfen sollten, der von malloc- Funktionen zurückgegeben wird , dann lade ich Sie ein, zu lesen: Warum ist es wichtig zu überprüfen, was malloc zurückgegeben hat .
Warnung 9
Die folgende Warnung wird aus denselben Gründen wie die vorherige verursacht. Es stimmt, es weist auf einen etwas anderen Fehler hin.
V769Der Zeiger 'middle_begin_' im Ausdruck 'middle_begin_ + (count.size () - 2)' könnte nullptr sein. In diesem Fall ist der resultierende Wert sinnlos und sollte nicht verwendet werden. Überprüfen Sie die Zeilen: 553, 552. search_trie.cc 553
template <class Quant, class Bhiksha> class TrieSearch {
....
private:
....
Middle *middle_begin_, *middle_end_;
....
};
template <class Quant, class Bhiksha>
uint8_t *TrieSearch<Quant, Bhiksha>::SetupMemory(....)
{
....
middle_begin_
= static_cast<Middle*>(malloc(sizeof(Middle) * (counts.size() - 2)));
middle_end_ = middle_begin_ + (counts.size() - 2);
....
}
Wie im vorherigen Beispiel wird hier mit der Funktion malloc Speicher zugewiesen . Der zurückgegebene Zeiger wird ohne Überprüfung auf nullptr im arithmetischen Ausdruck verwendet. Leider macht das Ergebnis eines solchen Ausdrucks keinen Sinn und ein völlig nutzloser Wert wird im Feld middle_end_ gespeichert.
Warnung 10
Und schließlich wurde das meiner Meinung nach interessanteste Beispiel in der in DeepSpeech enthaltenen Kenlm-Bibliothek gefunden:
V1061 Das Erweitern des Namespace 'std' kann zu undefiniertem Verhalten führen. size_iterator.hh 210
// Dirty hack because g++ 4.6 at least wants
// to do a bunch of copy operations.
namespace std {
inline void iter_swap(util::SizedIterator first,
util::SizedIterator second)
{
util::swap(*first, *second);
}
} // namespace std
Der Trick, der im Kommentar "schmutziger Trick" genannt wird, ist wirklich schmutzig. Der Punkt ist, dass eine solche Erweiterung des Standard-Namespace zu undefiniertem Verhalten führen kann.
Warum? Weil der Inhalt des Standard- Namespace ausschließlich vom Normungsausschuss festgelegt wird. Aus diesem Grund verbietet der internationale Sprachstandard C ++ ausdrücklich die Erweiterung von std auf diese Weise.
Der letzte in g ++ 4.6 unterstützte Standard ist C ++ 03. Hier ist ein übersetztes Zitat aus dem endgültigen Arbeitsentwurf von C ++ 03(siehe Punkt 17.6.4.2.1): "Das Verhalten eines C ++ - Programms ist undefiniert, wenn es dem std-Namespace oder dem std-verschachtelten Namespace Deklarationen oder Definitionen hinzufügt, sofern nicht anders angegeben." Dieses Zitat gilt für alle nachfolgenden Standards (C ++ 11, C ++ 14, C ++ 17 und C ++ 20).
Ich schlage vor zu überlegen, wie Sie den problematischen Code aus unserem Beispiel beheben können. Die erste logische Frage: Was sind diese "Fälle, für die das Gegenteil angezeigt ist"? Es gibt verschiedene Situationen, in denen die Standarderweiterung nicht zu undefiniertem Verhalten führt. Weitere Informationen zu all diesen Situationen finden Sie auf der Dokumentationsseite für die V1061-Diagnose . Jetzt ist es für uns wichtig, dass einer dieser Fälle die Hinzufügung einer Spezialisierung der Funktionsvorlage ist.
weilstd hat bereits eine Funktion namens iter_swap (Hinweis: eine Vorlagenfunktion). Es ist logisch anzunehmen, dass der Programmierer seine Funktionen erweitern wollte, damit er mit dem Typ util :: SizedIterator arbeiten kann . Aber hier ist das Pech: Anstatt der Funktionsvorlage eine Spezialisierung hinzuzufügen , hat der Programmierer einfach eine normale Überladung geschrieben . Es hätte so geschrieben werden sollen:
namespace std {
template <>
inline void iter_swap(util::SizedIterator first,
util::SizedIterator second)
{
util::swap(*first, *second);
}
} // namespace std
Dieser Code ist jedoch auch nicht so einfach. Der Punkt ist, dass dieser Code nur bis zum C ++ 20-Standard gültig ist. Ja, es wurde auch darauf hingewiesen, dass die Spezialisierung von Funktionsvorlagen zu undefiniertem Verhalten führt (siehe endgültigen C ++ 20-Arbeitsentwurf , Abschnitt 16.5.4.2.1). Und da dieser Code zu einer Bibliothek gehört, wird er wahrscheinlich früher oder später mit dem Flag -std = C ++ 20 erstellt . PVS-Studio unterscheidet übrigens, welche Version des Standards im Code verwendet wird, und gibt abhängig davon eine Warnung aus oder gibt sie nicht aus. Überzeugen Sie sich selbst: Beispiel für C ++ 17 , Beispiel für C ++ 20 .
In der Tat können Sie viel einfacher machen. Um den Fehler zu beheben, müssen Sie nur Ihre eigene Definition von iter_swap übertragenauf denselben Namespace, der die SizedIterator- Klasse definiert . In diesem Fall müssen Sie an den Stellen, an denen iter_swap aufgerufen wird , "using std :: iter_swap;" hinzufügen. Es stellt sich wie folgt heraus (die Definition der SizedIterator- Klasse und der util :: swap () -Funktion wurden der Einfachheit halber geändert):
namespace util
{
class SizedIterator
{
public:
SizedIterator(int i) : m_data(i) {}
int& operator*()
{
return m_data;
}
private:
int m_data;
};
....
inline void iter_swap(SizedIterator first,
SizedIterator second)
{
std::cout << "we are inside util::iter_swap" << std::endl;
swap(*first, *second);
}
}
int main()
{
double d1 = 1.1, d2 = 2.2;
double *pd1 = &d1, *pd2 = &d2;
util::SizedIterator si1(42), si2(43);
using std::iter_swap;
iter_swap(pd1, pd2);
iter_swap(si1, si2); // "we are inside util::iter_swap"
return 0;
}
Jetzt wählt der Compiler unabhängig die erforderliche Überladung der Funktion iter_swap basierend auf einer Argument Search (ADL) aus. Für die SizedIterator- Klasse wird die Version aus dem Namespace util aufgerufen , und für andere Typen wird die Version aus dem Namespace std aufgerufen . Der Beweis ist auf dem Link . Darüber hinaus müssen in Bibliotheksfunktionen keine "using" hinzugefügt werden: Da sich der Code bereits in std befindet , wählt der Compiler weiterhin die richtige Überladung aus.
Und dann - voila - funktioniert die benutzerdefinierte Funktion iter_swap wie es sollte ohne "schmutzige Tricks" und andere Hexerei :)

Fazit
Damit ist mein Artikel abgeschlossen. Ich hoffe, dass die Fehler, die ich gefunden habe, für Sie interessant waren und dass Sie etwas Neues und Nützliches für sich selbst gelernt haben. Wenn Sie bis zu diesem Punkt gelesen haben, wünsche ich Ihnen aufrichtig einen sauberen und ordentlichen Code ohne Fehler. Lassen Sie Fehler Ihre Projekte umgehen!
PS Wir halten es für eine schlechte Praxis, eigenen Code in den Namespace std zu schreiben. Was denkst du? Ich freue mich auf Ihre Antworten in den Kommentaren.
Wenn Sie in C, C ++, C # oder Java entwickeln und sich wie ich für das Thema statische Analyse interessieren, empfehle ich Ihnen, PVS-Studio selbst auszuprobieren. Sie können es unter dem Link herunterladen .

Wenn Sie diesen Artikel einem englischsprachigen Publikum zugänglich machen möchten, verwenden Sie bitte den Übersetzungslink: George Gribkov. Überprüfen des Codes von DeepSpeech oder Warum Sie nicht in den Namespace std schreiben sollten .
