parts/6.21.NamespaceIssues-BJL.md

6.21 Namespace Issues [BJL]

6.21.1 Applicability to language

The vulnerability described in ISO/IEC TR 24772-1:2019 clause 6.21 exists in C++. It can occur in particular when a used library changes its API. The situations where it exists are related to the following cases:

In the case of template specialization or non-identical definitions of the same entity in different translation units (ODR-violation), ill-formed code might be the result, however, a C++ compiler is not obliged to diagnose that situation, leading to undefined behaviour.

In the case of overloading and overriding cases, C++ compilers are required to diagnose an ambiguity if it exists.

However, overload resolution applies preference rules in order to select among multiple matching functions or function templates as a means to resolve the ambiguity among these functions. Hence, for calls that are not perfect matches, the user cannot guarantee in the presence of later changes which function is called, as another, better match can be introduced subsequently. The call in question then changes its binding without warning upon its next compilation. For cases, where the preference rules do not resolve the ambiguity, the resulting error message by the compiler avoids the vulnerability. Function template specializations are not considered during overload resolution, only the base template is considered.

void foo (long);

// void foo (int);
  
void bar ()
{
  foo (0);         // The call to 'foo(long)' requires implicitly conversion
                   // from 'int' to 'long'.   The function 'foo(int)' 
                   // would be a "better match" and so would silently
                   // be chosen when subsequently introduced
}

A new declaration can impact existing code in a number of situations involving the addition of:

A using directive broadens the possible scopes that will be examined for names during lookup. Where lookup searches a namespace referred to by a using directive, all names in that namespace will be visible some of which may be unwanted. A using declaration, on the other hand, declares only the specified name into the scope of the using declaration.

namespace NS1
{
  void f1 (int);
  void f2 (int);        // Added later
}
namespace NS2
{
  using namespace NS1;  // 'f1' needed
  void f2 (long);
  void bar ()
  {
    f1(0);              // Calls 'NS1::f1'
    f2(0);              // Unintentionally calls 'NS1::f2'
  }
}
namespace NS3
{
  using NS1::f1;        // 'f1' needed
  void f2 (long);
  void bar ()
  {
    f1(0);              // Calls 'NS1::f1'
    f2(0);              // Calls 'NS3::f2' as expected
  }
}

Overload resolution only considers conversions for the explicitly specified arguments and does not take default parameters into account:

void f1 (short, int = 0);
void f1 (int, short = 0);

void f2 ()
{
  f1 (1);       // calls 'f1(1, 0)' as '1 -> int' is better match than '1 -> short'
  f1 (1, 0);    // ambiguous, ill-formed, won't compile
}

The following example demonstrates a situation where the late addition of a better matching overload causes a silent change in the semantics of an existing program.

namespace NS
{
  struct A
  {
  };

  template < typename T > T foo ( T t )
  {
    return t;
  }
}

namespace NS2 // separately developed and included from a header file
{
   struct B
   {
   };
// This code will be added later
//  template < typename T > T * foo ( T * t )
//  {
//    return t;
//  }
}

using namespace NS2;
using namespace NS;

void bar()
{
  A * a;
  B * b;
  foo (a); // After the commented-out code is added to NS2, the binding of foo changes silently from NS::foo to NS2::foo
}

This issue can be avoided by avoiding using namespace xxx{.cpp} and explicitly qualifying each call, such as NS::foo(a){.cpp}.

Analogously, when a more specialized template is added to an imported namespace where the more general template has already been provided in another namespace, preference rules will silently prefer the more specialized template.

A similar situation can occur when a conflict arises between compiler-synthesized or rewritten operators and explicitly created versions of those operators, as in the following example.

struct A
{
    bool operator==(A const &) const { return true; }   
};

// Evil hijacking of !=
// bool operator != (A const &, A const &) { return true; } // #1

void bar (A const & a) {
  a != a;                                                    // #2
}

In the above example, the declaration of operator== will have a corresponding synthesised operator!= generated by the compiler, since there is no suitable user-declared !=. If the operator!= becomes visible, then the code at #2 uses the user-declared operator!= instead of the synthesized one, which can lead to a silent and unexpected change of behaviour. This is particularly risky when the operator is declared outside of the immediate visibility of the original definition.

6.21.2 Avoidance mechanisms for language users

To avoid the vulnerability or mitigate its ill effects, C++ software developers can: