EXPLORATION 68

image

Names, Namespaces, and Templates

The basics of using namespaces and templates are straightforward and easy to learn. Taking advantage of argument-dependent lookup (ADL) is also simple: declare free functions and operators in the same namespace as your classes. But sometimes life isn’t so simple. Especially when using templates, you can get stuck in strange corners, and the compiler issues bizarre and useless messages, and you realize you should have spent more time studying names, namespaces, and templates beyond the basics.

The detailed rules can be excruciatingly complicated, because they must cover all the pathological cases, for example, to explain why the following is legal (albeit resulting in some compiler warnings) and what it means:

enum X { X };
void XX(enum X X=::X) { if (enum X X=X) X; }

and why the following is illegal:

enum X { X } X;
void XX(X X=X) { if (X X=X) X; }

But rational programmers don’t write code in that manner, and some commonsense guidelines go a long way toward simplifying the complicated rules. Thus, this Exploration provides more details than earlier in the book but omits many picky details that matter only to entrants in obfuscated C++ contests.

Common Rules

Certain rules apply to all types of name lookup. (Subsequent sections will examine the rules particular to certain contexts). The basic rule is that the compiler must know what a name means when it sees the name in source code.

Most names must be declared earlier in the file (or in an included header) than where the name is used. The only exception is in the definition of a member function: a name can be another member of the same class, even if the member is declared later in the class definition than the use of that name. Names must be unique within a scope, except for overloaded functions and operators. The compiler issues an error if you attempt to declare two names that would conflict, such as two variables with the same name in the same scope, or a data member and member function with the same name in a single class.

Functions can have multiple declarations with the same name, following the rules for overloading, that is, argument number or types must be different, or const qualification must be different for member functions.

Access rules (public, private, or protected) have no effect on name lookup rules for members of a class (nested types and typedefs, data members, and member functions). The usual name lookup rules identify the proper declaration, and only then does the compiler check whether access to the name is permitted.

Whether a name is that of a virtual function has no effect on name lookup. The name is looked up normally, and once the name is found, then the compiler checks whether the name is that of a virtual function. If so, and if the object is accessed via a reference or pointer, the compiler generates the necessary code to perform a runtime lookup of the actual function.

Name Lookup in Templates

Templates complicate name lookup. In particular, a name can depend on a template parameter. Such a dependent name has different lookup rules than nondependent names. Dependent names can change meaning according to the template arguments used in a template specialization. One specialization may declare a name as a type, and another may declare it as a function. (Of course, such a programming style is highly discouraged, but the rules of C++ must allow for such possibilities.)

Lookup of nondependent names follows the usual name lookup rules where the template is defined. Lookup of dependent names may include lookup in namespaces associated with the template instantiation, in addition to the normal rules that apply where the template is defined. Subsequent sections provide additional details, according to the kind of name lookup being performed.

Three Kinds of Name Lookup

C++ defines three kinds of name lookup: member access operators; names prefaced by class, enumeration, or namespace names; and bare names.

  • A class member access operator is . or ->. The left-hand side is an expression that yields an object of class type, reference to an object, or pointer to an object. The dot (.) requires an object or reference, and -> requires a pointer. The right-hand side is a member name (data member or member function). For example, in the expression cout.width(3), cout is the object, and width is the member name.
  • A class, enumeration, or namespace name may be followed by the scope operator (::) and a name, such as std::string. The name is said to be qualified by the class, enumeration, or namespace name. No other kind of name may appear to the left of the scope operator. The compiler looks up the name in the scope of the class, enumeration, or namespace. The name itself may be another class, enumerator, or namespace name, or it may be a function, variable, or typedef name. For example, in std::chrono::system_clock::duration, std and chrono are namespace names; sytem_clock is a class name; and duration is a member typedef.
  • A plain identifier or operator is called an unqualified name. The name can be a namespace name, type name, function name, or object name, depending on context. Different contexts have slightly different lookup rules.

The next three sections describe each style of name lookup in more detail.

Member Access Operators

The simplest rules are for member access operators. The left-hand side of the member access operator (. or ->) determines the context for the lookup. The object must have class type (or pointer or reference to a class type), and the name on the right-hand side must be a data member or member function of that class or of an ancestor class. The search begins with the declared type of the object and continues with its base class (or classes, searching multiple classes from left to right, in order of declaration), and their base classes, and so on, stopping at the first class with a matching name.

If the name is a function, the compiler collects all declarations of the same name in the same class and chooses one function according to the rules of function and operator overloading. Note that the compiler does not consider any functions in ancestor classes. The name lookup stops as soon as the name is found. If you want a base class’s names to participate in operator overloading, use a using declaration in the derived class to bring the base class names into the derived class context.

In the body of a member function, the left-hand object can be the this keyword, which is a pointer to the object on the left-hand side of the member access operator. If the member function is declared with the const qualifier, this is a pointer to const. If a base class is a template parameter or depends on a template parameter, the compiler does not know which members may be inherited from the base class until the template is instantiated. You should use this-> to access an inherited member, to tell the compiler that the name is a member name, and the compiler will look up the name when it instantiates the template.

Listing 68-1 demonstrates several uses for member access operators.

Listing 68-1.  Member Access Operators

#include <cmath>
#include <iostream>
 
template<class T>
class point2d {
public:
   point2d(T x, T y) : x_(x), y_(y) {}
   virtual ~point2d() {}
   T x() const { return x_; }
   T y() const { return y_; }
   T abs() const { return std::sqrt(x() * x() + y() * y()); }
   virtual void print(std::ostream& stream) const {
      stream << '(' << x() << ", " << y() << ')';
   }
private:
   T x_, y_;
};
 
template<class T>
class point3d : public point2d<T> {
public:
   point3d(T x, T y, T z) : point2d<T>(x, y), z_(z) {}
   T z() const { return z_; }
   T abs() const {
      return static_cast<T>(std::sqrt( this->x() * this->x() +
                 this->y() * this->y() +
                 this->z() * this->z()));
   }
   virtual void print(std::ostream& stream) const {
      stream << '(' << this->x() << ", " << this->y() << ", " << z() << ')';
   }
private:
   T z_;
};
 
template<class T>
std::ostream& operator<<(std::ostream& stream, point2d<T> const& pt)
{
   pt.print(stream);
   return stream;
}
 
int main()
{
   point3d<int> origin{0, 0, 0};
   std::cout << "abs(origin) = " << origin.abs() << ' ';
 
   point3d<int> unit{1, 1, 1};
   point2d<int>* ptr{ &unit };
   std::cout << "abs(unit) = " << ptr->abs() << ' ';
   std::cout << "*ptr = " << *ptr << ' ';
}

The main() function uses member name lookup the way you are used to seeing it. This usage is simple to understand, and you have been using it throughout this book. The use of member name lookup in point3d::abs(), however, is more interesting. The use of this-> is required, because the base class, point2d<T> depends on the template parameter, T.

The operator<< function takes a reference to a point2d instance and calls its print function. The virtual function is dispatched to the real function, which in this case is point3d::print. You know how this works, so this is just a reminder of how the compiler looks up the name print in the point2d class template, because that is the type of the pt function parameter.

Qualified Name Lookup

A qualified name uses the scope (::) operator. You have been using qualified names from the very first program. The name std::string is qualified, which means the name string is looked up in a context specified by the std:: qualifier. In this simple class, std names a namespace, so string is looked up in that namespace.

The qualifier can also be a class name or the name of a scoped enumeration. Class names can nest, so the left- and right-hand side of the scope operator may be a class name. If the left-hand name is that of an enumerated type, the right-hand name must be an enumerator in that type.

The compiler starts its search with the left-most name. If the left-most name starts with a scope operator (e.g., ::std::string), the compiler looks up that name in the global scope. Otherwise, it uses the usual name lookup rules for unqualified names (as described in the next section) to determine the scope that it will use for the right-hand side of the scope operator. If the right-hand name is followed by another scope operator, the identified name must be that of a namespace, class, or scoped enumeration, and the compiler looks up the right-hand name in that scope. The process repeats until the compiler has looked up the right-most name.

Within a namespace, a using directive tells the compiler to search in the target namespace as well as the namespace that contains the using directive. In the following example, the qualified name ns2::Integer tells the compiler to search in namespace ns2 for name Integer. Because ns2 contains a using directive, the compiler also searches in namespace ns1, and so finds the Integer typedef.

namespace ns1 { typedef int Integer; }
namespace ns2 { using namespace ns1; }
namespace ns3 { ns2::Integer x; }

A using declaration is slightly different. A using directive affects which namespaces the compiler searches to find a name. A using declaration doesn’t change the set of namespaces to search but merely adds one name to the containing namespace. In the following example, the using declaration brings the name Integer into namespace ns2, just as though the typedef were written in ns2.

namespace ns1 { typedef int Integer; }
namespace ns2 { using ns1::Integer; }
namespace ns3 { ns2::Integer x; }

When a name depends on a template parameter, the compiler must know whether the name is that of a type or something else (function or object), because it affects how the compiler parses the template body. Because the name is dependent, it could be a type in one specialization and a function in another. So you have to tell the compiler what to expect. If the name should be a type, preface the qualified name with the keyword typename. Without the typename keyword, the compiler assumes the name is that of a function or object. You need the typename keyword with a dependent type, but it doesn’t hurt if you provide it before a nondependent type.

Listing 68-2 shows several examples of qualified names.

Listing 68-2.  Qualified Name Lookup

#include <chrono>
#include <iostream>
 
namespace outer {
   namespace inner {
      class base {
      public:
         int value() const { return 1; }
         static int value(long x) { return static_cast<int>(x); }
      };
   }
 
   template<class T>
   class derived : public inner::base {
   public:
      typedef T value_type;
      using inner::base::value;
      static value_type example;
      value_type value(value_type i) const { return i * example; }
   };
 
   template<class T>
   typename derived<T>::value_type derived<T>::example= 2;
}
 
template<class T>
class more_derived : public outer::derived<T>{
public:
   typedef outer::derived<T> base_type;
   typedef typename base_type::value_typevalue_type;
   more_derived(value_type v) : value_{this->value(v)} {}
   value_type get_value() const { return value_; }
private:
   value_type value_;
};
  
int main()
{
   std::chrono::system_clock::time_pointnow{ std::chrono::system_clock::now()};
   std::cout<< now.time_since_epoch().count() << ' ';
 
   outer::derived<int> d;
   std::cout<< d.value() << ' ';
   std::cout<< d.value(42L) << ' ';
   std::cout<< outer::inner::base::value(2) << ' ';
 
   more_derived<int> md(2);
   std::cout<< md.get_value() << ' ';
}

The standard chrono library is different from most other parts of the standard library, by using a nested namespace, std::chrono. Within that namespace, the system_clock class has a member typedef, time_point, and a function, now().

The now() function is static, so it is called as a qualified name, not by using a member access operator. Although it operates on an object, it behaves as a free function. The only difference between now() and a completely free function is that its name is qualified by a class name instead of a namespace name. Exploration 50 briefly touched on static functions. They are not used often, but this is one of those instances when such a function is useful. The now() function is declared with the static qualifier, which means the function does not need an object, the function body has no this pointer, and the usual way to call the function is with a qualified name.

A data member may be static too. (Go back to Exploration 40 for a refresher.) A member function (ordinary or static) can refer to a static data member normally, or you can use a qualified name to access the member from outside the class. One additional difference between a static member function and a free function is that a static member function can access private static members of the class. If you declare a static data member, you must also provide a definition for that member, typically in the same source file where member functions are defined. Recall that a non-static data member does not have a definition, because the instance of the data member is created when the containing object is created. Static data members are independent of any objects, so they must be defined independently too.

In Listing 68-2, the first call to d.value() calls base::value(). Without the using declaration in derived, the only signature for value() is value(value_type i), which doesn’t match value(), and so would result in a compile error. But the usinginner::base::value declaration injects the value name from inner::base, adding the functions value() and value(long) as additional functions overloading the name value. Thus, when the compiler looks up d.value(), it searches all three signatures to find the value() that the using declaration injected into derived. The second call, d.value(42L), invokes value(long). Even though the function is static, it can be called using a member access operator. The compiler ignores the object but uses the object’s type as the context for looking up the name. The final call to value(2) is qualified by the class name, so it searches only the value functions in class base, finds value(long), and converts the int 2 to long.

In the most_derived class template, the base class depends on the template parameter, T. Thus, the base_type typedef is dependent. The compiler needs to know what base_type::value_type is, so the typename keyword informs the compiler that value_type is a type.

Unqualified Name Lookup

A name without a member access operator or a qualifier is unqualified. The precise rules for looking up an unqualified name depend on the context. For example, inside a member function, the compiler searches other members of the class and then inherited members, before searching in the class’s namespace and then outer namespaces.

The rules are commonsense, and the details are germane, primarily to compiler writers who must get all the details correct. For most programmers, you can get by with common sense and a few guidelines.

  • Names are looked up first in the local scope, then in outer scopes.
  • In a class, names are looked up among the class members, then in ancestor classes.
  • In a template, the compiler must resolve every unqualified object and type name when the template is defined, without regard to the instantiation context. Thus, it does not search a base class for names, if the base class depends on a template parameter.
  • If a name cannot be found in the class or ancestors, or if the name is called outside of any class context, the compiler searches the immediate namespace, then outer namespaces.
  • If a name is a function or operator, the compiler also searches the namespaces of the function arguments and their outer namespaces, according to the rules of argument-dependent lookup (ADL). In a template, namespaces for the template declaration and instantiation are searched.

Listing 68-3 contains several examples of unqualified name lookup.

Listing 68-3.  Unqualified Name Lookup

#include <iostream>
 
namespace outer {
   namespace inner {
      struct point { int x, y; };
      inline std::ostream& operator<<( std::ostream& stream, pointconst& p)
      {
         stream<< '(' << p.x << ", " << p.y << ')';
         return stream;
      }
   }
}
 
typedef int Integer;
 
int main()
{
   const int multiplier{2};
   for (Integer i : { 1, 2, 3}) {
      outer::inner::point p{ i, i* multiplier};
      std::cout << p<< ' ';
   }
}

Argument-Dependent Lookup

The most interesting form of unqualified name lookup is argument-dependent lookup. As the name implies, the compiler looks up a function name in the namespaces determined by the function arguments. As a guideline, the compiler assembles the broadest, most inclusive set of classes and namespaces that it reasonably can, to maximize the search space for a name.

More precisely, if a search finds a member function, the compiler does not apply ADL, and the search stops there. Otherwise, the compiler assembles an additional set of classes and namespaces to search and combines them with the namespaces it searches for normal lookup. The compiler builds this additional set by checking the types of the function arguments. For each function argument, the class or namespace in which the argument’s type is declared is added to the set. In addition, if the argument’s type is a class, the ancestor classes and their namespaces are also added. If the argument is a pointer, the additional classes and namespaces are those of the base type. If you pass a function as an argument, that function’s parameter types are added to the search space. When the compiler searches the additional ADL-only namespaces, it searches only for matching function names, ignoring types and variables.

If the function is a template, the additional classes and namespaces include those where the template is defined and where the template is instantiated.

Listing 68-4 shows several examples of argument-dependent lookup. The listing uses the definition of rational from Exploration 52, in the numeric namespace.

Listing 68-4.  Argument-Dependent Name Lookup

#include <cmath>
#include <iostream>
#include "rational.hpp"
 
namespace data {
  template<class T>
  struct point {
    T x, y;
  };
  template<class Ch, class Tr, class T>
  std::basic_ostream<Ch, Tr>& operator<<(std::basic_ostream<Ch, Tr>& stream, point<T> const& pt)
  {
    stream << '(' << pt.x << ", " << pt.y << ')';
    return stream;
  }
  template<class T>
  T abs(point<T> const& pt) {
    using namespace std;
    return sqrt(pt.x * pt.x + pt.y * pt.y);
  }
}
 
namespace numeric {
   template<class T>
   rational<T> sqrt(rational<T> r)
   {
     using std::sqrt;
     return rational<T>{sqrt(static_cast<double>(r))};
   }
}
 
int main()
{
   using namespace std;
   data::point<numeric::rational<int>> a{ numeric::rational<int>{1, 2}, numeric::rational<int>{2, 4} };
   std::cout << "abs(" << a << ") = " << abs(a) << ' ';
}

Start with main() and follow the name lookups.

The first name is data, which is looked up as an unqualified name. The compiler finds the namespace data, declared in the global namespace. The compiler then knows to look up point in the data namespace, and it finds the class template. Similarly, the compiler looks up numeric and then rational.

The compiler constructs a and adds the name to the local scope.

The compiler looks up std and then cout, which it finds, because cout was declared in the <iostream> header. Next, the compiler looks up the unqualified name, a, which it finds in the local scope. But then it has to look up abs.

The compiler searches first in the local scope and then in the global scope. The using directive tells the compiler to search namespace std too. That exhausts the possibilities for normal lookup, so the compiler must turn to argument-dependent lookup.

The compiler assembles its set of scopes to search. First it adds data to the namespaces to search. Because point is a template, the compiler also searches the namespace where the template is instantiated, which is the global namespace. It already searched there, but that’s okay. Once the set is complete, the compiler searches for and finds abs in namespace data.

In order to instantiate the template abs, with the template argument numeric::rational<int>, the compiler must lookup operator*. It cannot find a declaration in the local scope, namespace data, namespace std, or the global namespace. Using argument-dependent lookup, it finds operator* in the numeric namespace, where rational is declared. It performs the same lookup for operator+.

In order to find sqrt, the compiler again uses argument-dependent lookup. When we last visited the rational class, it lacked a sqrt function, so Listing 68-4 provides a crude one. It converts the rational to a double, calls sqrt, and then converts back to rational. The compiler finds sqrt in namespace std.

Finally, the compiler must again apply argument-dependent lookup for operator<<. When the compiler compiles operator<< in point, it doesn’t know about operator<< for rational, but it doesn’t have to until the template is instantiated. As you can see, writing code that takes advantage of argument-dependent lookup is straightforward, if you follow simple guidelines. The next Exploration takes a closer look at the rules for resolving overloaded functions and operators. Again, you will find complicated rules that can be made simple by following some basic guidelines.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset