The vulnerability as described in ISO/IEC TR 24772-1:2019 clause 6.20 exists in C++, except for the second issue of limited identifier length. In C++ all characters in an identifier are significant.
C++ provides the scope resolution operator ::{.cpp}
to access identifiers from non-local scopes.
Overloading and specialization of functions is a cornerstone of C++ generic programming. In this context, the reuse of function names is essential. See clause 6.41 for inheritance issues associated with name reuse.
Overloaded function names and operators considered in an expression are not restricted to a simple scope hierarchy, because of argument-dependent lookup (ADL). In generic code the unqualified function or operator selected can come from a scope based on the type of the arguments and not from the current scope hierarchy. The rules for which namespaces are eligible for lookup of unqualified functions and operators are intricate, but required to make overloaded operators work.
In addition, if implicit conversions can happen on arguments, the overload selected by ADL can be different from programmer expectation even in non-generic code, especially when an argument is of a type that can be implicitly converted to another type where a corresponding overload is defined. Visibility on a namespace-level of such an operator overload may make it eligible, even if neither argument matches the parameter types directly. In the best case this leads to a compile error due to ambiguities, but it can also result in perfectly compiling code executing an unexcepted overload.
The following example demonstrates part of the problem:
#include <iostream>
#include <typeinfo>
namespace Y {
template <typename T>
void print(T i){
std::cout << typeid(T).name()<< ":" << i ;
}template <typename T>
void println(T x){
// expects to call Y::print
print(x); std::cout<<'\n';
}
} namespace X {
struct A{
double){}
A(friend // make this a hidden friend
std::ostream & operator << (std::ostream & out, A const &a){
return out << "An A as expected\n";
}
};void print(A a){ // not expected to be called by println
std::cout << "Surprise happens!";
}
}int main(){
3.14};
X::A a{42); // i:42 - calls Y::print
Y::println(std::cout << a; // An A as expected - calls X::operator<<
// Surprise happens! - calls X::print
Y::println(a); 42u);// u:42 - calls Y::print
Y::println( }
The above code calls the overload print(A)
from println since it is pulled in by ADL. On the other hand, ADL is required to work to allow the output operator for type X::A
to work.
The consideration of implicit conversions together with ADL can be suppressed by defining operator overloads as class members or as hidden friends. The latter is achieved by declaring all corresponding overloads as friend
functions in the class that take the class’ objects as arguments. Generic base classes can provide mix-in facilities for hidden friends by taking the argument type that is the derived class as template parameter.
To avoid the vulnerability or mitigate its ill effects, C++ software developers can:
Use the avoidance mechanisms of ISO/IEC 24772-1 clause 6.20.5, with the exclusion of guidance related to truncated identifiers.
Qualify names to disambiguate potential conflicts between names introduced from different scopes.
Document argument-dependent lookup usage where name qualification is not desirable.
Limit the visibility of overloaded operators or functions for class types by defining them as member functions or hidden friends.
Place overloaded operators that are not class members and cannot be provided as hidden friends together with their argument type in a namespace that is not the global namespace,so that they are picked up by ADL.
Use modern integrated development environments that inform about the declaration of any identifier occurrence.
Enable compiler diagnostics that inform about the hiding of declarations.
DCL60-CPP. Obey the one-definition
rule (6.21)
DCL40-C. Do not create incompatible declarations of the same
function or object (6.21)