7. Namespaces and Run-Time Type Identification

Lecture



Namespace is a set, by which is meant a model, an abstract container, or an environment created for the logical grouping of unique identifiers (that is, names).

An identifier defined in a namespace is associated with that namespace. The same identifier can be defined independently in several namespaces. Thus, the meaning associated with an identifier defined in one namespace may or may not be the same as that of the same identifier defined in another namespace. Languages that support namespaces define rules specifying which namespace an identifier belongs to (that is, its definition).

For example, Katya works at company X, and her ID (short for Identifier) as an employee is 123. Oleg works at company Y, and his ID is also 123. The only thing (from the point of view of some accounting system) that makes Katya and Oleg distinguishable when their IDs coincide is that they belong to different companies. The distinction between the companies in this case constitutes a system of different namespaces (one company, one namespace). Having two employees in a company with the same ID causes serious problems when they are used: for example, given a payment slip that names an employee with ID 123, it would be very difficult to determine which employee the slip is intended for.

Large databases can contain hundreds and thousands of identifiers. Namespaces (or similar structures) implement a mechanism for hiding local identifiers. Their purpose is to group logically related identifiers into corresponding namespaces, thereby making the system modular. Limiting the visibility of variables can also be done by specifying its storage class.

Operating systems and many modern programming languages provide support for their own namespace model: they use directories (or folders) as a namespace model. This allows two files with the same name to exist (as long as they are in different directories). In some programming languages (for example, C++ and Python), the identifiers of the namespaces are themselves associated with the corresponding namespaces. Therefore, in these languages namespaces can be nested within one another, forming a tree of namespaces. The root of such a tree is called the global namespace.

7. Namespaces and Run-Time Type Identification

Emulating namespaces

In programming languages without their own support for namespaces, namespaces can be emulated by extension, using conventions for naming identifiers. For example, C libraries such as Libpng often use a fixed prefix for all functions and variables that is part of their external interface. Libpng supports external identifiers such as:

png_create_write_struct

png_get_signature

png_read_row

png_set_invalid

This gives a reasonable guarantee that the identifiers will be unique and thus can be used in large programs without fear of identifier name collisions.

The disadvantages of emulating namespaces include the following

  • There is no proper accounting for nested namespaces; identifiers become excessively long.
  • Programmers or organizations may use sharply incompatible naming conventions, thereby potentially causing greater confusion.
  • Complex operations, or query operations over groups of identifiers based on the namespaces in which they are declared, are handled far too inefficiently or are not feasible at all.
  • All references to identifiers must in fact be made with the full name of the namespace .. Languages with direct support for namespaces usually give the programmer the ability to declare in advance that they want to use some (or even all) identifiers in the program from only one namespace, which they can subsequently use without specifying the namespace they belong to.

Name conflicts in programming

A name conflict occurs when two identical identifiers are in the same scope, and the compiler cannot tell which of the two should be used in a particular situation. The compiler or linker will issue an error, since they do not have enough information to resolve this ambiguity. As programs grow in size, the number of identifiers also grows, and consequently the likelihood of name conflicts increases as well.

Let us consider an example of such a conflict. boo.h and doo.h are header files with functions that do different things but have the same names and parameters.

boo.h:

7. Namespaces and Run-Time Type Identification

doo.h:

7. Namespaces and Run-Time Type Identification

main.cpp:

7. Namespaces and Run-Time Type Identification

If boo.h and doo.h are compiled separately, everything goes without incident. However, by combining them in one program, we include two different functions with the same names and parameters in one scope (the global one), and this in turn leads to a name conflict. As a result, the compiler issues an error. The concept of namespaces was added to the C++ language precisely to solve such problems.

What is a namespace?

A namespace defines an area of code in which all identifiers are guaranteed to be unique. By default, global variables and ordinary functions are defined in the global namespace. For example:

7. Namespaces and Run-Time Type Identification

The global variable g_z and the function boo() are defined in the global namespace.

In the example above, when the files boo.h and doo.h are included, both versions of doOperation() were placed in the global namespace, which is precisely why the name conflict occurred.

To avoid such situations, in which two independent objects have identifiers that may conflict with each other when used together, C++ allows you to declare your own namespaces using the namespace keyword. Everything declared inside a user-defined namespace belongs only to that namespace (and not to the global one).

Let us rewrite the header files from the example above, this time using namespace:

boo.h:

7. Namespaces and Run-Time Type Identification

doo.h:

7. Namespaces and Run-Time Type Identification

Now doOperation() from the file boo.h is in the namespace Boo, and doOperation() from the file doo.h is in the namespace Doo. Let us see what happens when main.cpp is recompiled:

7. Namespaces and Run-Time Type Identification

The result is another error:

C:\Projects\Test.cpp(15) : error C2065: 'doOperation' : undeclared identifier

What happened is that when we tried to call the function doOperation(), the compiler looked in the global namespace for a definition of doOperation(). However, since neither of our versions of doOperation() is in the global namespace, the compiler could not find a definition of doOperation() at all!

There are two different ways to tell the compiler which version of doOperation() to use: through the scope resolution operator or with using statements (we will discuss them in the next lesson).

Accessing a namespace through the scope resolution operator (::)

The first way to tell the compiler to look for an identifier in a particular namespace is to use the name of the required namespace together with the scope resolution operator (::) and the required identifier.

For example, let's tell the compiler to use the version of doOperation() from the Boo namespace:

7. Namespaces and Run-Time Type Identification

Result:

9

If we wanted to use the version of doOperation() from the Doo namespace:

7. Namespaces and Run-Time Type Identification

Result:

1

The scope resolution operator is useful because it lets us select a specific namespace. We can even do the following:

7. Namespaces and Run-Time Type Identification

Result:

9
1

This operator can also be used without any prefix (for example, ::doOperation). In that case, we are referring to the global namespace.

Namespaces with the same name

Namespaces may be declared in several places (either in several files or in several places within one file). Everything inside a single namespace block is considered part of that block only.

add.h:

7. Namespaces and Run-Time Type Identification

subtract.h:

7. Namespaces and Run-Time Type Identification

main.cpp:

7. Namespaces and Run-Time Type Identification

Everything works as intended.

The C++ standard library makes extensive use of this feature, since all the header files it contains implement their functionality inside the std namespace.

Aliases and nested namespaces

Namespaces can be nested inside other namespaces. For example:

7. Namespaces and Run-Time Type Identification

Note that because Doo is inside Boo, g_x is accessed through Boo::Doo::g_x.

Since this is not always convenient or efficient, C++ allows you to create aliases for namespaces:

7. Namespaces and Run-Time Type Identification

It is worth noting that namespaces in C++ were not designed as a way to implement an information hierarchy — they were designed as a mechanism for preventing name conflicts. As evidence of this, the entire Standard Template Library lives in a single namespace, std::.

Nesting namespaces is not recommended, because when used carelessly it increases the likelihood of errors and further complicates the program logic.

19.1. Run-time type identification

Run-time type identification (abbreviated run-time type information, run-time type identification, RTTI) is a mechanism in some programming languages that makes it possible to determine the data type of a variable or object while the program is running.

* RTTI allows programs that manipulate objects through pointers or references to base classes to obtain the actual derived type of the object being addressed. To support RTTI, C++ has two operators: the dynamic_cast operator supports type conversions at run time, providing safe navigation of the class hierarchy. It lets us convert a pointer to a base class into a pointer to a class derived from it, and also convert an lvalue referring to a base class into a reference to a derived class, but only if the conversion succeeds;

* the typeid operator lets us obtain the actual derived type of an object addressed by a pointer or reference.

However, to obtain information about the type of a derived class, the operand of either dynamic_cast or typeid must be of a class type that has at least one virtual function. Thus, the RTTI operators are run-time events for classes with virtual functions and compile-time events for all other types. In this section we will look at their capabilities in more detail. Using RTTI turns out to be necessary when implementing applications such as debuggers or object databases, where the type of the objects the program manipulates becomes known only at run time by examining the RTTI information stored together with the object types. However, it is better to use the static type system of C++, since it is safer and more efficient.

19.1.1. The dynamic_cast operator

The dynamic_cast operator can be used to convert a pointer referring to an object of a class type into a pointer to a class type from the same hierarchy. It is also used to convert an lvalue of a class-type object into a reference to a class type from the same hierarchy. Unlike the other type conversion facilities in C++, a cast performed with dynamic_cast takes place while the program is running. If the pointer or lvalue cannot be converted to the target type, dynamic_cast fails. When casting a pointer, failure is indicated by returning a null value. If an lvalue cannot be converted to a reference type, an exception is thrown. Below we give examples of this operator failing.

Before looking at dynamic_cast in more detail, let's see why it is needed. Suppose a program uses a class library to represent various categories of company employees. The classes in the hierarchy support member functions for computing salary:

class employee {
public:
virtual int salary();
};
class manager : public employee {
public:
int salary();
};
class programmer : public employee {
public:
int salary();
};
void company::payroll( employee *pe ) {
// uses pe-salary()
}

A company has different categories of employees. The parameter of the payroll() member function of the company class is a pointer to an employee object, which may address one of the types manager or programmer. Since payroll() calls the virtual member function salary(), the appropriate overriding function defined in the manager or programmer class is invoked, depending on which object the pointer addresses.

Suppose the employee class no longer meets our needs, and we want to modify it by adding another member function, bonus(), used together with salary() when calculating the payroll. To do this, we need to add the new member function to the classes that make up the employee hierarchy:

class employee {
public:
virtual int salary(); // salary
virtual int bonus(); // bonus
};
class manager : public employee {
public:
int salary();
};
class programmer : public employee {
public:
int salary();
int bonus();
};
void company::payroll( employee *pe ) {
// uses pe-salary() and pe-bonus()
}

If the pe parameter of payroll() points to an object of type manager, the virtual member function bonus() from the base class employee is called, since it is not overridden in the manager class. If pe points to an object of type programmer, the virtual member function bonus() from the programmer class is called.

After adding new virtual functions to a class hierarchy, all the member functions have to be recompiled. bonus() can be added if we have access to the source code of the member functions in the employee, manager and programmer classes. However, if the hierarchy was obtained from an independent vendor, it is quite possible that we have only the header files describing the interface of the library classes and the object files with their implementation, while the source code of the member functions is unavailable. In that case, recompiling the entire hierarchy is impossible.

If we want to extend the functionality of the class library without adding new virtual member functions, we can use the dynamic_cast operator.

This operator is used to obtain a pointer to a derived class so that we can work with those of its members that are not otherwise accessible. Suppose we extend the library by adding a new member function bonus() to the programmer class. Its declaration can be included in the definition of programmer in the header file, and the function itself can be defined in one of our own source files:

class employee {
public:
virtual int salary();
};
class manager : public employee {
public:
int salary();
};
class programmer : public employee {
public:
int salary();
int bonus();
};

Recall that payroll() takes as its parameter a pointer to the base class employee. We can apply the dynamic_cast operator to obtain a pointer to the derived class programmer and use it to call the member function bonus():

void company::payroll( employee *pe )
{
programmer *pm = dynamic_cast programmer* ( pe );
// if pe points to an object of type programmer,
// then dynamic_cast succeeds and pm will
// point to the beginning of the programmer object
if ( pm ) {
// use pm to call programmer::bonus()
}
// if pe does not point to an object of type programmer,
// then dynamic_cast fails
// and pm will contain 0
else {
// use the member functions of class employee
}
}

The operator

dynamic_cast

casts its operand pe to the type programmer*. The conversion succeeds if pe refers to an object of type programmer, and fails otherwise: in that case the result of dynamic_cast is 0.

Thus, the dynamic_cast operator performs two operations at once. It checks whether the requested cast is feasible, and if so, performs it. The check is made while the program is running. dynamic_cast is safer than the other type-cast operations in C++, because it checks whether a correct conversion is possible.

If in the previous example pe really does point to an object of type programmer, the dynamic_cast operation succeeds and pm is initialized with a pointer to an object of type programmer. Otherwise, pm receives the value 0. By checking the value of pm, the function company::payroll() can find out whether pm points to a programmer object. If so, it calls the member function programmer::bonus() to calculate the programmer's bonus. If dynamic_cast fails, then pe points to an object of type manager, which means that a more general calculation algorithm that does not use the new member function programmer::bonus() must be applied.

The dynamic_cast operator is used to safely convert a pointer to a base class into a pointer to a derived class. Such an operation is often called downcasting. It is used when it is necessary to take advantage of features of the derived class that are absent from the base class. Manipulating objects of a derived class through pointers to the base class normally happens automatically, by means of virtual functions. Sometimes, however, it is impossible to use virtual functions. In such situations dynamic_cast offers an alternative solution, although this mechanism is more error-prone than virtualization and must be used with caution.

One possible mistake is to work with the result of dynamic_cast without first checking it for 0: a null pointer cannot be used to address a class object. For example:

void company::payroll( employee *pe )
{
programmer *pm = dynamic_cast( pe );
// potential error: pm is used without checking its value
static int variablePay = 0;
variablePay += pm-bonus();
// ...
}

The result returned by dynamic_cast should always be checked before it is used as a pointer. A more correct definition of the function company::payroll() might look like this:

void company::payroll( employee *pe )
{
// perform dynamic_cast and check the result
if ( programmer *pm = dynamic_cast( pe ) ) {
// use pm to call programmer::bonus()
}
else {
// use the member functions of class employee
}
}

The result of the dynamic_cast operation is used to initialize the variable pm inside the conditional expression of the if statement. This is possible because declarations in conditions yield values. The branch corresponding to a true condition is executed if pm is not zero: we know that the dynamic_cast operation succeeded and pe points to a programmer object. Otherwise the result of the declaration is 0 and the else branch is executed. Since the operator and the check of its result are now in a single program statement, it is impossible to accidentally insert any code between the execution of dynamic_cast and the check, so pm will be used only when it contains a valid pointer.

In the previous example, the dynamic_cast operation converts a pointer to a base class into a pointer to a derived class. It can also be used to convert an lvalue of a base class type into a reference to a derived type. The syntax of this use of dynamic_cast is as follows:

dynamic_cast Type & &( lval )

where Type& is the target type of the conversion, and lval is an lvalue of a base class type. The operand lval is successfully cast to the type Type& only if lval actually refers to an object of a class for which one of the derived classes has the type Type.

Since there are no null references (see section 3.6), it is impossible to check whether the operation succeeded by comparing the result (that is, the reference returned by the dynamic_cast operator) with zero. If references are used instead of pointers, the condition

if ( programmer *pm = dynamic_cast programmer* ( pe ) )

cannot be rewritten as

if ( programmer &pm = dynamic_cast& programmer& &( pe ) )

To report an error when casting to a reference type, the dynamic_cast operator throws an exception. Consequently, the previous example can be written as follows:

#include typeinfo
void company::payroll( employee &re )
{
try {
programmer &rm = dynamic_cast& programmer & &( re );
// use rm to call programmer::bonus()
}
catch ( std::bad_cast ) {
// use the member functions of class employee
}
}

If the reference variant of dynamic_cast fails, an exception of type bad_cast is thrown. The bad_cast class is defined in the standard library; to refer to it, the header file must be included in the program. (We will look at the standard library exceptions in the next section.)

When should the reference variant of dynamic_cast be used instead of the pointer variant? It depends only on the programmer's preference. When it is used, it is impossible to ignore a type-cast error and work with the result without checking it (as is possible with the pointer variant); on the other hand, the use of exceptions increases the run-time overhead of the program

See also

  • [[b6216]]
  • [[b9259]]
  • [[b4565]]
  • [[b5336]]
  • [[b9259]]

See also

created: 2015-12-20
updated: 2026-09-29
297



Was this answer useful?
Choose a quick rating so we can improve the next answer for you.
How satisfied are you?


Comments

To leave a comment

If you have any suggestion, idea, thanks or comment, feel free to write. We really value feedback and are glad to hear your opinion.
To reply

Lectures and tutorial on "C ++ (C plus plus)"

Terms: C ++ (C plus plus)