The auto Keyword
The
auto
keyword has a number of completely different uses:
➤
Deducing a function’s return type, as explained earlier in this chapter.
➤
Structured bindings, as explained earlier in this chapter.
➤
Deducing the type of an expression, as discussed later in this section.
➤
Deducing the type of non-type template parameters, see Chapter 12.
➤
decltype(auto)
, see Chapter 12.
➤
Alternative function syntax, see Chapter 12.
➤
Generic lambda expressions, see Chapter 18.
auto
can be used to tell the compiler to automatically deduce the type of a variable at compile time.
The following line shows the simplest use of the
auto
keyword in that context:
auto x = 123; // x will be of type int
In this example, you don’t win much by typing
auto
instead of
int
; however, it becomes useful
for more complicated types. Suppose you have a function called
getFoo()
that has a complicated
return type. If you want to assign the result of calling
getFoo()
to a variable, you can spell out the
complicated type, or you can simply use
auto
and let the compiler figure it out:
This has the added benefit that you can easily change the function’s return type without having to
update all the places in the code where that function is called.
However, using
auto
to deduce the type of an expression strips away reference and
const
qualifiers.
Suppose you have the following function:
#include <string>
const std::string message = "Test";
const std::string& foo()
{
return message;
}
You can call
foo()
and store the result in a variable with the type specified as
auto
, as follows:
Because
auto
strips away reference and
const
qualifiers,
f1
is of type
string
, and thus a
copy
is
made. If you want a
const
reference, you can explicitly make it a reference and mark it
const
, as
follows:
const auto& f2 = foo();
WARNING
Always keep in mind that
auto
strips away reference and
const
qualifiers, and thus creates a copy! If you do not want a copy, use
auto&
or
const auto&
.
The decltype Keyword
The
decltype
keyword takes an expression as argument, and computes the type of that expression,
as shown here:
int x = 123;
decltype(x) y = 456;
In this example, the compiler deduces the type of
y
to be
int
because that is the type of
x
.
The difference between
auto
and
decltype
is that
decltype
does not strip reference and
const
qualifiers. Take again the function
foo()
returning a
const
reference to a
string
. Defining
f2
using
decltype
as follows results in
f2
being of type
const string&
, and thus no copy is made.
decltype(foo()) f2 = foo();
On first sight,
decltype
doesn’t seem to add much value. However, it is pretty powerful in the con
text of templates, discussed in Chapters 12 and 22.
C++ AS AN OBJECT-ORIENTED LANGUAGE
If you are a C programmer, you may have viewed the features covered so far in this chapter as con
venient additions to the C language. As the name C++ implies, in many ways the language is just a
“better C.” There is one major point that this view overlooks: unlike C, C++ is an object-oriented
language.
Object-oriented programming (OOP) is a very different, arguably more natural, way to write code.
If you are used to procedural languages such as C or Pascal, don’t worry. Chapter 5 covers all the
background information you need to know to shift your mindset to the object-oriented paradigm.
If you already know the theory of OOP, the rest of this section will get you up to speed (or refresh
your memory) on basic C++ object syntax.
Defining Classes
A
class
defines the characteristics of an object. In C++, classes are usually defined in a header file
(.h), while their definitions usually are in a corresponding source file (.cpp).
A basic class definition for an airline ticket class is shown in the following example. The class can
calculate the price of the ticket based on the number of miles in the flight and whether or not the
customer is a member of the “Elite Super Rewards Program.” The definition begins by declaring the
class name. Inside a set of curly braces, the
data members
(properties) of the class and its
methods
(behaviors) are declared. Each data member and method is associated with a particular access level:
public
,
protected
, or
private
. These labels can occur in any order and can be repeated. Members
that are
public
can be accessed from outside the class, while members that are
private
cannot be
accessed from outside the class. It’s recommended to make all your data members
private
, and if
needed, to give access to them with
public
getters and setters. This way, you can easily change the
representation of your data while keeping the
public
interface the same. The use of
protected
is
explained in the context of inheritance in Chapters 5 and 10.
#include <string>
class AirlineTicket
{
public:
AirlineTicket();
~AirlineTicket();
double calculatePriceInDollars() const;
const std::string& getPassengerName() const;
void setPassengerName(const std::string& name);
int getNumberOfMiles() const;
void setNumberOfMiles(int miles);
bool hasEliteSuperRewardsStatus() const;
void setHasEliteSuperRewardsStatus(bool status);
private:
std::string mPassengerName;
int mNumberOfMiles;
bool mHasEliteSuperRewardsStatus;
};
This book follows the convention to prefix each data member of a class with a lowercase ‘m’, such as
mPassengerName
.
NOTE
To follow the
const
-correctness principle, it’s always a good idea to
declare member functions that do not change any data member of the object as
being
const
. These member functions are also called “inspectors,” compared to
“mutators” for non-
const
member functions.
The method that has the same name as the class with no return type is a
constructor
. It is automati
cally called when an object of the class is created. The method with a tilde (~) character followed by
the class name is a
destructor
. It is automatically called when the object is destroyed.
There are two ways of initializing data members with a constructor. The recommended way is
to use a
constructor initializer
, which follows a colon after the constructor name. Here is the
AirlineTicket
constructor with a constructor initializer:
AirlineTicket::AirlineTicket()
: mPassengerName("Unknown Passenger")
, mNumberOfMiles(0)
, mHasEliteSuperRewardsStatus(false)
{
}
A second way is to put the initializations in the body of the constructor, as shown here:
AirlineTicket::AirlineTicket()
{
// Initialize data members
mPassengerName = "Unknown Passenger";
mNumberOfMiles = 0;
mHasEliteSuperRewardsStatus = false;
}
If the constructor is only initializing data members without doing anything else, then there is no real
need for a constructor because data members can be initialized directly inside the class definition.
For example, instead of writing an
AirlineTicket
constructor, you can modify the definition of the
data members in the class definition as follows:
private:
std::string mPassengerName = "Unknown Passenger";
int mNumberOfMiles = 0;
bool mHasEliteSuperRewardsStatus = false;
If your class additionally needs to perform some other types of initialization, such as opening a file,
allocating memory, and so on, then you still need to write a constructor to handle those.
Here is the destructor for the
AirlineTicket
class:
AirlineTicket::~AirlineTicket()
{
// Nothing much to do in terms of cleanup
}
This destructor doesn’t do anything, and can simply be removed from this class. It is just shown
here so you know the syntax of destructors. Destructors are required if you need to perform some
cleanup, such as closing files, freeing memory, and so on. Chapters 8 and 9 discuss destructors in
more detail.
The definitions of some of the
AirlineTicket
class methods are shown here:
double AirlineTicket::calculatePriceInDollars() const
{
if (hasEliteSuperRewardsStatus()) {
// Elite Super Rewards customers fly for free!
return 0;
}
// The cost of the ticket is the number of miles times 0.1.
// Real airlines probably have a more complicated formula!
return getNumberOfMiles() * 0.1;
}
const string& AirlineTicket::getPassengerName() const
{
return mPassengerName;
}
void AirlineTicket::setPassengerName(const string& name)
{
mPassengerName = name;
}
Using Classes
The following sample program makes use of the
AirlineTicket
class. This example shows the
creation of a stack-based
AirlineTicket
object as well as a heap-based one:
AirlineTicket myTicket; // Stack-based AirlineTicket
myTicket.setPassengerName("Sherman T. Socketwrench");
myTicket.setNumberOfMiles(700);
double cost = myTicket.calculatePriceInDollars();
cout << "This ticket will cost $" << cost << endl;
// Heap-based AirlineTicket with smart pointer
auto myTicket2 = make_unique<AirlineTicket>();
myTicket2->setPassengerName("Laudimore M. Hallidue");
myTicket2->setNumberOfMiles(2000);
myTicket2->setHasEliteSuperRewardsStatus(true);
double cost2 = myTicket2->calculatePriceInDollars();
cout << "This other ticket will cost $" << cost2 << endl;
// No need to delete myTicket2, happens automatically
// Heap-based AirlineTicket without smart pointer (not recommended)
AirlineTicket* myTicket3 = new AirlineTicket();
// ... Use ticket 3
delete myTicket3; // delete the heap object!
The preceding example exposes you to the general syntax for creating and using classes. Of course,
there is much more to learn. Chapters 8, 9, and 10 go into more depth about the specific C++ mech
anisms for defining classes.
UNIFORM INITIALIZATION
Before C++11, initialization of types was not always uniform. For example, take the following defi
nition of a circle, once as a structure, and once as a class:
struct CircleStruct
{
int x, y;
double radius;
};
class CircleClass
{
public:
CircleClass(int x, int y, double radius): mX(x), mY(y), mRadius(radius) {}
private:
int mX, mY;
double mRadius;
};
In pre-C++11, initialization of a variable of type
CircleStruct
and a variable of type
CircleClass
looks different:
CircleStruct myCircle1 = {10, 10, 2.5};
CircleClass myCircle2(10, 10, 2.5);
For the structure version, you can use the
{...}
syntax. However, for the class version, you need to
call the constructor using function notation
(...)
.
Since C++11, you can more uniformly use the
{...}
syntax to initialize types, as follows:
CircleStruct myCircle3 = {10, 10, 2.5};
CircleClass myCircle4 = {10, 10, 2.5};
The definition of
myCircle4
automatically calls the constructor of
CircleClass
. Even the use of the
equal sign is optional, so the following is identical:
CircleStruct myCircle5{10, 10, 2.5};
CircleClass myCircle6{10, 10, 2.5};
Uniform initialization is not limited to structures and classes. You can use it to initialize anything in
C++. For example, the following code initializes all four variables with the value 3:
int a = 3;
int b(3);
int c = {3}; // Uniform initialization
int d{3}; // Uniform initialization
Uniform initialization can be used to perform zero-initialization* of variables; you just specify an empty set of curly braces, as shown here:
int e{}; // Uniform initialization, e will be 0
Using uniform initialization prevents
narrowing
. C++ implicitly performs narrowing, as shown here:
void func(int i) { /* ... */ }
int main()
{
int x = 3.14;
func(3.14);
}
In both cases, C++ automatically truncates 3.14 to 3 before assigning it to
x
or calling
func()
. Note
that some compilers
might
issue a warning about this narrowing, while others won’t. With uniform
initialization, both the assignment to
x
and the call to
func()
must
generate a compiler error if your
compiler fully conforms to the C++11 standard:
void func(int i) { /* ... */ }
int main()
{
int x = {3.14}; // Error because narrowing
func({3.14}); // Error because narrowing
}
Uniform initialization can be used to initialize dynamically allocated arrays, as shown here:
int* pArray = new int[4]{0, 1, 2, 3};
It can also be used in the constructor initializer to initialize arrays that are members of a class.
class MyClass
{
public:
MyClass() : mArray{0, 1, 2, 3} {}
private:
int mArray[4];
};
Uniform initialization can be used with the Standard Library containers as well—such as the
std::vector
, as demonstrated later in this chapter.
Direct List Initialization versus Copy List Initialization
There are two types of initialization that use braced initializer lists:
➤
Copy list initialization.
T obj = {arg1, arg2, ...};
➤
Direct list initialization.
T obj {arg1, arg2, ...};
In combination with auto type deduction, there is an important difference between copy- and direct
list initialization introduced with C++17.
Starting with C++17, you have the following results:
// Copy list initialization
auto a = {11}; // initializer_list<int>
auto b = {11, 22}; // initializer_list<int>
// Direct list initialization
auto c {11}; // int
auto d {11, 22}; // Error, too many elements.
Note that for copy list initialization, all the elements in the braced initializer must be of the same
type. For example, the following does not compile:
auto b = {11, 22.33}; // Compilation error
In earlier versions of the standard (C++11/14), both copy- and direct list initialization deduce an
initializer_list<>
:
// Copy list initialization
auto a = {11}; // initializer_list<int>
auto b = {11, 22}; // initializer_list<int>
// Direct list initialization
auto c {11}; // initializer_list<int>
auto d {11, 22}; // initializer_list<int>
THE STANDARD LIBRARY
C++ comes with a Standard Library, which contains a lot of useful classes that can easily be used
in your code. The benefit of using these classes is that you don’t need to reinvent certain classes and
you don’t need to waste time on implementing things that have already been implemented for you.
Another benefit is that the classes available in the Standard Library are heavily tested and verified
for correctness by thousands of users. The Standard Library classes are also tuned for high perfor
mance, so using them will most likely result in better performance compared to making your own
implementation.
A lot of functionality is available to you in the Standard Library. Chapters 16 to 20 provide more
details; however, when you start working with C++ it is a good idea to understand what the
Standard Library can do for you from the very beginning. This is especially important if you are a C
programmer. As a C programmer, you might try to solve problems in C++ the same way you would
solve them in C. However, in C++ there is probably an easier and safer solution to the problem that
involves using Standard Library classes.
You already saw some Standard Library classes earlier in this chapter—for example,
std::string
,
std::array
,
std::vector
,
std::unique_ptr
, and
std::shared_ptr
. Many more classes are
introduced in Chapters 16 to 20.