Commentary: User-Defined Types
Steven Zeil
This lessons is devoted to the simpler ways in which programmers can define their type names.
Programmers do, in fact, frequently introduce new data types in their programs. Usually that is done by declaring new structured types (“structs” or “classes”), which will be discussed in a section.
In this section, we are looking at some more basic mechanisms that may not be used as often, but are still important. And we also look at the idea of “name spaces”, an attempt to manage the problems that can arise when programmers start adding hundreds (or more) of new identifiers to their programs.
1 typedef
Many times we describe types by describing how they are constructed, e.g., an array of integers, a reference to a string, etc. When written in an actual programming language, those kinds of descriptions are called type expressions.
This is an analogy to the more common expressions that we might write like “x+y”, but instead of using thyings like “+” to operate on values to get a new value, we are now thinking of phrases like “array of” as operators that are applied to data types to create new data types.
A typedef assigns a name to a type expression.
Sometimes we do this to better indicate the “role” that our data is playing. For example, look at the number “1.0”. Is that a measure of weight, or a measure of speed? Obviously, it could be either, or something else entirely. We can’t tell without some context.
A typedef can supply that context:
typedef double Speed;
typedef double weight;
⋮
Speed train1, train2;
Weight load1, load2;
means the exact same thing as
double train1, train2, load1, load2;
but the first can be more expressive.
Typedefs are sometimes used to provide abbreviations for complicated type expressions that we would not want to type (or read!) over and over. As a beginning C++ programmer, you are still far away from needing to deal with type expressions like std::map<std::string,int>::value_type
, but when the time comes, you will be very happy to write
typedef std::map<std::string,int>::value_type CountedWord;
⋮
CountedWords wc1 = ...;
⋮
CountedWords wc2 = ...;
⋮
vocabulary.insert(CountedWords("the",24));
rather than
std::map<std::string,int>::value_type wc1 = ...;
⋮
std::map<std::string,int>::value_type wc2 = ...;
⋮
vocabulary.insert(std::map<std::string,int>::value_type("the",24));
2 enum
enum
types are common to many programming languages. They are a bit frustrating in C++, however, because of some important limitations:
-
There’s no easy way to print an
enum
value. Among other things, this makes debugging code that uses them rather awkward. -
There’s no easy way to read an enum value.
They can, however, be used as a convenient way of grouping related named constants, as array indices, and as a basis for loops.
3 namespaces
3.1 Name spaces and namespaces
Whenever we are writing code, there is a certain set of names that we have access to. That set changes based on where in the code we are, and based upon what wee have already written.
For example, when I write
⋮ ➀
int x = 2;
⋮ ➁
we can make use of the name “x” in the region marked ➁, but not in the region marked ➀. Or, on the other hand, if we wanted to introduce a different variable/type/function named “x”, we probably can’t do that in either place.
Scopes affect this set of available names as well. If I have written
int foo(int i)
{
int x = 2*i;
return x;
}
⋮
int x = foo(22);
there’s no conflict between my two declarations of “x” because the scope of the first one is limited to its surrounding { }
.
Intuitively we think of all of the already-declared names of things as floating freely in “name space”. A “name space conflict” or “name space clash” occurs when we want to declare something new and give it an appropriate name, only to discover that our chosen name is already in our name space, that something declared elsewhere already is using that name.
This can be a real problem because the number of predefined things can be quite large, if not because of our own code then just because of the C++ standard library. And if we (or the company that we program for) have obtained and installed some extra libraries, those could be adding still more names to our name space. It can become a real challenge to know what is taken and what is not. (And, as libraries are updated, they may change the sets of names they use.)
Example 1: Naming Functions example 1Suppose that you were writing a bunch of functions that you have tentatively named “min”, “max”, “sum”, and “distance”. How many of those are already in the C++ standard library?
AnswerBut if you didn’t know that, don’t feel bad. It’s hard to keep track of all the simple or natural names that we might want to use in our own code but that are already in the standard library.
This is a common problem in most programming languages.
In the early days of C++, I worked with two different libraries, written by different people, both of which declared a new data type named “string”. That made it all but impossible to actually use both libraries in the same program. Later, when the C++ standard library added its own string
type, both libraries had to rename theirs.
The namespace
(without the blank) is a C++ construct to help manage your name space by allowing you to divide the “space” up into different, named, regions. The best-known of these is std
, the C++ standard library.
Example 2: Naming Functions example 2Suppose that you were writing a bunch of functions that you have tentatively named “min”, “max”, and “distance”. Some spoilsport has already told you that those names are taken in the C++ standard library. Can you do it anyway?
Yes, you can. Those names are already in the
std
namespace. But if you writeint min (int x, int y);
it goes into the default, unnamed namespace. Your function can be accessed as
min
or even as::min
, and the standard function asstd::min
.
Example 3: Naming Functions example 3Suppose that you are going to use a typedef to name a “Speed” type, but someone else in the company has already used that name – but you really think it’s the only reasonable name for your data type. Can you do it anyway?
Yes, just put it in a namespace of your own:
namespace myCoolNamespace { typedef ... Speed; }
and you can refer to your data type as “
myCoolNamespace::Speed
”.
3.2 Shortening the names
Often we would prefer not to type out the entire long name of an identifier. The using
statement allows us to “import” certain names in their abbreviated form into our current name space.
There are two forms of using
statement.
First, if we only want to use the shortened form of a few specific identifiers, we can import them with:
using
full-name-of-identifier ;
For example, if we have previously done
namespace myCoolNamespace {
typedef ... Speed;
}
and then later write
using myCoolNamespace::Speed;
then, afterwards, we can refer to that data type as simply “Speed
”.
If we want to use the short form of a large number of identifiers from a common namespace
, then we can import all of the names in that namespace with the statement
using namespace
namespace-name ;
You’ve probably already seen this used many times as “using namespace
std
”; For example, we might write the “Hello world” program as
#include <iostream>
#include <string>
const std::string greeting = "Hello, world!";
int main()
{
std::cout << greeting << std::endl;
}
or
#include <iostream>
#include <string>
using namespace std;
const string greeting = "Hello, world!";
int main()
{
cout << greeting << endl;
}
The effects of the using
statements are limited to the score in which they appear. In particular, if a using
appears within { }
, its effect ends at the closing }
. So it’s possible to permit short names within a particular function body, for example, without allowing them through the whole program.