Introduction
We’ve discussed the RAII idiom. Some language usages and programming idioms in C++ might seem foreign or pointless at first glance, but they do have a purpose. In this chapter, we will explore a few of these odd usages and idioms to understand where they came from and why they are used.
You will commonly see C++ increment an integer by using the syntax ++i instead of i++. The reason for this is partly historic, partly useful, and partly a sort of secret handshake. One of the common places you will see this is in a for loop (e.g., for (int i = 0; i < someNumber; ++i) { ... }
). Why do C++ programmers use ++i rather than i++? Let’s consider what these two operators mean.
int i = 0; int x = ++i; int y = i++;
In the previous code, when all three statements finish executing, i will be equal to 2. But what will x and y equal? They will both equal 1. This is because the pre-increment operator in the statement, ++i, means “increment i and give the new value of i as the result.” So when assigning x its value, i goes from 0 to 1, and the new value of i, 1, is assigned to x. The post-increment operator in the statement i++ means “increment i and give the original value of i as the result.” So when assigning y its value, i goes from 1 to 2, and the original value of i, 1, is assigned to y.
If we were to decompose that sequence of instructions step-by-step as written, eliminating the pre-increment and post-increment operators and replacing them with regular addition, we would realize that to perform the assignment to y, we need an extra variable to hold the original value of i. The result would be something like this:
int i = 0; // int x = ++i; i = i + 1; int x = i; // int y = i++; int magicTemp = i; i = i + 1; int y = magicTemp;
Early compilers, in fact, used to do things like that. Modern compilers now determine that there are no observable side effects to assigning to y first, so the assembly code they generate, even without optimization, will typically look like the assembly-language equivalent of this C++ code:
int i = 0; // int x = ++i; i = i + 1; int x = i; // int y = i++; int y = i; i = i + 1;
In some ways, the ++i syntax (especially within a for loop) is a holdover from the early days of C++, and even C before it. Knowing that other C++ programmers use it, employing it yourself lets others know you have at least some familiarity with C++ usages and style—the secret handshake. The useful part is that you can write a single line of code, int x = ++i;
, and get the result you desire rather than writing two lines of code: i++;
followed by int x = i;
.
Tip: While you can save a line of code here and there with tricks such as capturing the pre-increment operator’s result, it’s generally best to avoid combining a bunch of operations in a single line. The compiler isn’t going to generate better code, since it will just decompose that line into its component parts (the same as if you had written multiple lines). Hence, the complier will generate machine code that performs each operation in an efficient manner, obeying the order of operations and other language constraints. All you’ll do is confuse other people who have to look at your code. You’ll also introduce a perfect situation for bugs, either because you misused something or because someone made a change without understanding the code. You’ll also increase the likelihood that you yourself will not understand the code if you come back to it six months later.
Concerning Null - Use nullptr
At the beginning of its life, C++ adopted many things from C, including the usage of binary zero as the representation of a null value. This has created countless bugs over the years. I’m not blaming Kernighan, Ritchie, Stroustrup, or anyone else for this; it’s amazing how much they accomplished when creating these languages given the computers available in the 70s and early 80s. Trying to figure out what things will be problems when creating a computer language is an extremely difficult task.
Nonetheless, early on, programmers realized that using a literal 0 in their code could produce confusion in some instances. For example, imagine you wrote:
int* p_x = p_d; // More code here... p_x = 0;
Did you mean to set the pointer to null as written (i.e. p_x = 0;) or did you mean to set the pointed-to value to 0 (i.e. *p_x = 0;)? Even with code of reasonable complexity, the debugger could take significant time to diagnose such errors.
The result of this realization was the adoption of the NULL preprocessor macro: #define NULL 0
. This would help reduce errors, if you saw *p_x = NULL;
or p_x = 0;
then, assuming you and the other programmers were using the NULL macro consistently, the error would be easier to spot, fix, and the fix would be easier to verify.
But because the NULL macro is a preprocessor definition, the compiler would never see anything other than 0 due to textual substitution; it could not warn you about possibly erroneous code. If someone redefined the NULL macro to another value, all sorts of additional problems could result. Redefining NULL is a very bad thing to do, but sometimes programmers do bad things.
C++11 has added a new keyword, nullptr, which can and should be used in place of 0, NULL, and anything else when you need to assign a null value to a pointer or check to see if a pointer is null. There are several good reasons to use it.
The nullptr keyword is a language keyword; it is not eliminated by the preprocessor. Since it passes through to the compiler, the compiler can detect errors and generate usage warnings that it couldn’t detect or generate with the literal 0 or any macros.
It also cannot be redefined either accidentally or intentionally, unlike a macro such as NULL. This eliminates all the errors that macros can introduce.
Lastly, it provides future proofing. Having binary zero as the null value was a practical decision when it was made, but it was arbitrary nonetheless. Another reasonable choice might have been to have null be the max value of an unsigned native integer. There are positives and negatives to such a value, but there’s nothing I know of that would have made it unusable.
With nullptr, it suddenly becomes feasible to change what null is for a particular operating environment without making changes to any C++ code that has fully adopted nullptr. The compiler can take a comparison with nullptr, or the assignment of nullptr to a pointer variable, and generate whatever machine code the target environment requires from it. Trying to do the same with a binary 0 would be very difficult, if not impossible. If in the future someone decides to design a computer architecture and operating system that adds a null flag bit for all memory addresses to designate null, modern C++ could support that because of nullptr.
Strange-Looking Boolean Equality Checks
You will commonly see people write code such as if (nullptr == p_a) { ... }
. I have not followed that style in the samples because it simply looks wrong to me. In the 18 years I have been writing programs in C and C++, I have never had a problem with the issue this style avoids. Nonetheless, other people have had such problems. This style might possibly be part of the style rules you are required to follow; therefore, it is worth discussing.
If you wrote if (p_a = nullptr) { ... }
instead of if (p_a == nullptr) { ... }
, then your program would assign the null value to p_a and the if statement would always evaluate to false. C++, owing to its C heritage, allows you to have an expression that evaluates to any integral type within the parentheses of a control statement, such as if. C# requires that the result of any such expression be a Boolean value. Since you cannot assign a value to something like nullptr or to constant values, such as 3 and 0.0F, if you put that R-value on the left side of an equality check, the compiler will alert you to the error. This is because you would be assigning a value to something that cannot have a value assigned to it.
For this reason, some developers have taken up writing their equality checks this way. The important part is not which style you choose, but that you are aware that an assignment inside of something such as an if expression is valid in C++. That way, you know to look out for such problems.
Whatever you do, do not intentionally write statements like if (x = 3) { ... }
. That is very bad style, which makes your code harder to understand and more prone to developing bugs.
throw()
and noexcept(bool expression)
Note: As of Visual Studio 2012 RC, the Visual C++ compiler accepts but does not implement exception specifications. However, if you include a throw() exception specification, the compiler will likely optimize away any code it would otherwise generate to support unwinding when an exception is thrown. Your program may not run properly if an exception is thrown from a function marked with throw(). Other compilers that do implement throw specifications will expect them to be marked properly, so you should implement proper exception specifications if your code needs to be compiled with another compiler.
Note: Exception specifications using the throw() syntax (called dynamic-exception specifications) are deprecated as of C++11. As such, they may be removed from the language in the future. The noexcept specification and operator are replacements for this language feature but are not implemented in Visual C++ as of Visual Studio 2012 RC.
C++ functions can specify via the throw() exception specification keyword whether or not to throw exceptions, and if so, what kind to throw.
For example, int AddTwoNumbers(int, int) throw();
declares a function that, due to the empty parentheses, states it does not throw any exceptions, excluding those it catches internally and does not re-throw. By contrast, int AddTwoNumbers(int, int) throw(std::logic_error);
declares a function that states it can throw an exception of type std::logic_error
, or any type derived from that.
The function declaration int AddTwoNumber(int, int) throw(...);
declares that it can throw an exception of any type. This syntax is Microsoft-specific, so you should avoid it for code that may need to be compiled with something other than the Visual C++ compiler.
If no specifier appears, such as in int AddTwoNumbers(int, int);
, then the function can throw any exception type. It is the equivalent of having the throw(...)
specifier.
C++11 added the new noexcept(bool expression) specification and operator. Visual C++ does not support these as of Visual Studio 2012 RC, but we will discuss them briefly since they will undoubtedly be added in the future.
The specifier noexcept(false)
is the equivalent of both throw(...)
and of a function without a throw specifier. For example, int AddTwoNumbers(int, int) noexcept(false);
is the equivalent of both int AddTwoNumber(int, int) throw(...);
and int AddTwoNumbers(int, int);
.
The specifiers noexcept(true)
and noexcept are the equivalent of throw()
. In other words, they all specify that the function does not allow any exceptions to escape from it.
When overriding a virtual member function, the exception specification of the override function in the derived class cannot specify exceptions beyond those declared for the type it is overriding. Let’s look at an example.
#include <stdexcept> #include <exception> class A { public: A(void) throw(...); virtual ~A(void) throw(); virtual int Add(int, int) throw(std::overflow_error); virtual float Add(float, float) throw(); virtual double Add(double, double) throw(int); }; class B : public A { public: B(void); // Fine, since not having a throw is the same as throw(...). virtual ~B(void) throw(); // Fine since it matches ~A. // The int Add override is fine since you can always throw less in // an override than the base says it can throw. virtual int Add(int, int) throw() override; // The float Add override here is invalid because the A version says // it will not throw, but this override says it can throw an // std::exception. virtual float Add(float, float) throw(std::exception) override; // The double Add override here is invalid because the A version says // it can throw an int, but this override says it can throw a double, // which the A version does not specify. virtual double Add(double, double) throw(double) override; };
Because the throw exception specification syntax is deprecated, you should only use the empty parentheses form of it, throw(), in order to specify that a particular function does not throw exceptions; otherwise, just leave it off. If you want to let others know what exceptions your functions can throw, consider using comments in your header files or in other documentation, making sure to keep them up-to-date.
noexcept(bool expression)
is also an operator. When used as an operator, it takes an expression that will evaluate to true if it cannot throw an exception, or false if it can throw an exception. Note that the result is a simple evaluation; it checks to see if all functions called are noexcept(true)
, and if there are any throw statements in the expression. If it finds any throw statements, even ones that you know are unreachable, (e.g., if (x % 2 < 0) { throw "This computer is broken"; }
) it can, nonetheless, evaluate to false since the compiler is not required to do a deep-level analysis.
Pimpl (Pointer to Implementation)
The pointer-to-implementation idiom is an older technique that has been getting a lot of attention in C++. This is good, because it is quite useful. The essence of the technique is that in your header file you define your class’ public interface. The only data member you have is a private pointer to a forward-declared class or structure (wrapped in a std::unique_ptr
for exception-safe memory handling), which will serve as the actual implementation.
In your source code file, you define this implementation class and all of its member functions and member data. The public functions from the interface call into the implementation class for its functionality. The result is that once you’ve settled on the public interface for your class, the header file never changes. Thus, the source code files that include the header will not need to be recompiled due to implementation changes that do not affect the public interface.
Whenever you want to make changes to the implementation, the only thing that needs to be recompiled is the source code file where that implementation class exists, rather than every source code file that includes the class header file.
Here is a simple sample.
Sample: PimplSample\Sandwich.h
#pragma once #include <memory> class SandwichImpl; class Sandwich { public: Sandwich(void); ~Sandwich(void); void AddIngredient(const wchar_t* ingredient); void RemoveIngredient(const wchar_t* ingredient); void SetBreadType(const wchar_t* breadType); const wchar_t* GetSandwich(void); private: std::unique_ptr<SandwichImpl> m_pImpl; };
Sample: PimplSample\Sandwich.cpp
#include "Sandwich.h" #include <vector> #include <string> #include <algorithm> using namespace std; // We can make any changes we want to the implementation class without // triggering a recompile of other source files that include Sandwich.h since // SandwichImpl is only defined in this source file. Thus, only this source // file needs to be recompiled if we make changes to SandwichImpl. class SandwichImpl { public: SandwichImpl(); ~SandwichImpl(); void AddIngredient(const wchar_t* ingredient); void RemoveIngredient(const wchar_t* ingredient); void SetBreadType(const wchar_t* breadType); const wchar_t* GetSandwich(void); private: vector<wstring> m_ingredients; wstring m_breadType; wstring m_description; }; SandwichImpl::SandwichImpl() { } SandwichImpl::~SandwichImpl() { } void SandwichImpl::AddIngredient(const wchar_t* ingredient) { m_ingredients.emplace_back(ingredient); } void SandwichImpl::RemoveIngredient(const wchar_t* ingredient) { auto it = find_if(m_ingredients.begin(), m_ingredients.end(), [=] (wstring item) -> bool { return (item.compare(ingredient) == 0); }); if (it != m_ingredients.end()) { m_ingredients.erase(it); } } void SandwichImpl::SetBreadType(const wchar_t* breadType) { m_breadType = breadType; } const wchar_t* SandwichImpl::GetSandwich(void) { m_description.clear(); m_description.append(L"A "); for (auto ingredient : m_ingredients) { m_description.append(ingredient); m_description.append(L", "); } m_description.erase(m_description.end() - 2, m_description.end()); m_description.append(L" on "); m_description.append(m_breadType); m_description.append(L"."); return m_description.c_str(); } Sandwich::Sandwich(void) : m_pImpl(new SandwichImpl()) { } Sandwich::~Sandwich(void) { } void Sandwich::AddIngredient(const wchar_t* ingredient) { m_pImpl->AddIngredient(ingredient); } void Sandwich::RemoveIngredient(const wchar_t* ingredient) { m_pImpl->RemoveIngredient(ingredient); } void Sandwich::SetBreadType(const wchar_t* breadType) { m_pImpl->SetBreadType(breadType); } const wchar_t* Sandwich::GetSandwich(void) { return m_pImpl->GetSandwich(); }
Sample: PimplSample\PimplSample.cpp
#include <iostream> #include <ostream> #include "Sandwich.h" #include "../pchar.h" using namespace std; int _pmain(int /*argc*/, _pchar* /*argv*/[]) { Sandwich s; s.AddIngredient(L"Turkey"); s.AddIngredient(L"Cheddar"); s.AddIngredient(L"Lettuce"); s.AddIngredient(L"Tomato"); s.AddIngredient(L"Mayo"); s.RemoveIngredient(L"Cheddar"); s.SetBreadType(L"a Roll"); wcout << s.GetSandwich() << endl; return 0; }
Conclusion
Best practices and idioms are essential for any language or platform so revisit this article to really take in what we've covered here. Next up are templates, a language feature allowing you to reuse your code.
This lesson represents a chapter from C++ Succinctly, a free eBook from the team at Syncfusion.
Comments