Introduction
C++ is not the language you learn in 12 lessons in one week. With the C++ standard spanning 1300 pages, you can still have things to learn after years of experience. I’d argue you could hardly count on your fingers the people that know everything the standard says.
In this article, I will walk through several language features that are probably less known to many C++ developers. Some of them are more useful than others, some could only confuse fellow developers and should not be used in real code.
Function try-catch
A try
-catch
statement can appear anywhere a statement can appear. However, it is also possible to have a try
-catch
statement at a function level.
bool foobar()
try
{
std::cout << "foobar" << std::endl;
throw std::exception("foobar error");
}
catch (std::exception const & e)
{
std::cout << e.what() << std::endl;
return false;
}
int main()
{
auto f = foobar1();
std::cout << std::boolalpha << f << std::endl;
}
Probably an example that makes more sense could look like this:
bool foobar()
try
{
return true;
}
catch (std::exception const & e)
{
std::cout << e.what() << std::endl;
return false;
}
However, this is no different than the usual that one would write:
bool foobar()
{
try
{
return true;
}
catch (std::exception const & e)
{
std::cout << e.what() << std::endl;
return false;
}
}
The function try
-catch
syntax makes sense and was introduced for constructors. The problem with constructors is that if a constructor throws the object is not fully constructor (since the constructor did not successfully finish execution) and the destructor for the object is not called. As a result, memory/resource leaks may occur.
struct foo
{
foo() { std::cout << "foo constructed" << std::endl; }
~foo() { std::cout << "foo destroyed" << std::endl; }
};
struct bar
{
bar() { throw std::exception("error in bar!"); }
};
struct foobar
{
foobar()
: m_foo(new foo()),
m_bar()
{
std::cout << "foobar constructed" << std::endl;
}
~foobar()
{
std::cout << "foobar destroyed" << std::endl;
}
private:
foo* m_foo;
bar m_bar;
};
int main()
{
try
{
foobar fb;
}
catch (std::exception const & e)
{
std::cout << e.what() << std::endl;
}
}
This only prints:
foo constructed
error in bar!
To correctly destroy the foo
object, you must try
-catch
the constructor initializer list.
foobar()
try
: m_foo(new foo()),
m_bar()
{
std::cout << "foobar constructed" << std::endl;
}
catch (...)
{
delete m_foo;
throw;
}
The new program will print:
foo constructed
foo destroyed
error in bar!
An alternative to this solution is to use smart pointers instead of naked pointers. In this case, the function try
-catch
is no longer necessary. The following implementation produces the same output:
struct foobar
{
foobar()
: m_foo(std::make_unique<foo>()),
m_bar()
{
std::cout << "foobar constructed" << std::endl;
}
~foobar()
{
std::cout << "foobar destroyed" << std::endl;
}
private:
std::unique_ptr<foo> m_foo;
bar m_bar;
};
Unnamed Namespaces
Namespaces are declarative regions that provide a scope to the identifiers it contains. Everybody is probably familiar with named namespaces, nested namespaces or the C++ 11 inline namespaces, but not everybody knows about unnamed (or anonymous namespaces). As the name implies, these are declared without a name and are used to make identifiers local to a translation unit.
namespace
{
void print(std::string message) {}
}
The above namespace is the equivalent to the following code:
namespace __uniquename__ {}
using namespace __uniquename__;
namespace __uniquename__
{
void print(std::string message) {}
}
The code in the namespace and the namespace itself is not visible outside the translation unit. This makes unnamed namespaces useful to avoid name collisions from different translation units.
Consider the following situation when two source files contain a function with the same name.
void print(std::string message)
{
std::cout << "[foo]" << message << std::endl;
}
void print(std::string message)
{
std::cout << "[bar]" << message << std::endl;
}
When you try to build this, you get a linking error. In VC++, the errors are:
foo.obj : error LNK2005: "void __cdecl print
(class std::basic_string<char,struct std::char_traits<char>,
class std::allocator<char> >)" (?print@@YAXV?$basic_string@DU?
$char_traits@D@std@@V?$allocator@D@2@@std@@@Z) already defined in bar.obj
cpp_test.exe : fatal error LNK1169: one or more multiply defined symbols found
You can easily solve this problem by putting the duplicate identifier/function in an unnamed namespace in one (or all) of the source files. Since one of the duplicate names is now fully qualified with a unique namespace name (generated by the compiler), the linkage error no longer occurs.
namespace
{
void print(std::string message)
{
std::cout << "[bar]" << message << std::endl;
}
}
void run()
{
print("running...");
}
Pure Virtual Functions With a Body
The presence of a pure virtual
function in a class declaration makes the class abstract (unlike other programming languages where you have to explicitly use a keyword for that). That means the class cannot be instantiated, and a derived class from that class must implement the pure virtual
function, otherwise it is also considered abstract.
A pure virtual
function is declared with =0
at the end.
struct base
{
virtual void run() = 0;
}
It is little known though that a pure virtual
function can actually have a body:
struct base
{
virtual void run() = 0;
};
void base::run()
{
std::cout << "base::run" << std::endl;
}
In VC++, the definition can be put in the declaration, but this is not allowed by the C++ standard (see paragraph 10.4).
[Note: A function declaration cannot provide both a pure-specifier and a definition —end note ] [ Example:
struct C {
virtual void f() = 0 { }; };
—end example ]
struct base
{
virtual void run() = 0 {
std::cout << "base::run" << std::endl;
}
};
Pure virtual
functions with a body are not often seen in practice, but there are a couple of cases when they are useful or mandatory:
lvalue Conditional Operator
The conditional operator allows you to write shorter conditional code.
auto a = 12;
auto b = 42;
auto max = a >= b ? a : b;
That is basically equivalent to:
auto max = a;
if (b > a) max = b;
However, the conditional operator can also be used as an l-value on the left side of an assignment operation.
c % 2 == 0 ? a : b = 1;
And you can very well have something like this:
c % 2 == 0 ? a : b = a >= b ? -a : -b;
Array Indexing (subscript Operator) is Commutative
Indexing an array is routine for everyone and is basically the same for all programming languages. However, in C++, you can swap the array and index and have code like this:
int arr[] = {1, 2, 3, 4};
std::cout << arr[1] << std::endl;
std::cout << 1[arr] << std::endl;
for (int i = 0; i < 4; ++i)
std::cout << i[arr] << std::endl;
This is because the expression array[index]
is equivalent to *(array + index)
and that is the same as *(index + array)
, which translates back to index[array]
.
This is emphasized in the standard in paragraph 5.2.1 (Subscripting):
A postfix expression followed by an expression in square brackets is a postfix expression. One of the expressions shall have the type “pointer to T” and the other shall have unscoped enumeration or integral type. The result is an lvalue of type “T.” The type “T” shall be a completely-defined object type.62 The expression E1[E2] is identical (by definition) to *((E1)+(E2)) [ Note: see 5.3 and 5.7 for details of * and + and 8.3.4 for details of arrays. —end note ]
and also clarified in paragraph 8.3.4.6:
[Note: Except where it has been declared for a class (13.5.5), the subscript operator [] is interpreted in such a way that E1[E2] is identical to *((E1)+(E2)). Because of the conversion rules that apply to +, if E1 is an array and E2 an integer, then E1[E2] refers to the E2-th member of E1. Therefore, despite its asymmetric appearance, subscripting is a commutative operation.
This commutativity does not apply to classes that have overloaded the subscript operator[], such as std::array.
Alternative Tokens (aka Digraphs)
The C++ standard defines alternative tokens for some operators and punctuators (some of them for the compatibility with the C++ standard).
%:include <iostream>
%:include <array>
%:include <vector>
%:include <algorithm>
using namespace std;
int main()
<%
vector<int> arr = <% 1, 3, 5, 7, 8 %>;
transform(
begin(arr), end(arr), begin(arr),
<::>(auto const v) <% return v * 2; %>);
for (auto i = begin(arr); i not_eq end(arr); ++i)
cout << *i << endl;
%>
That code is direct equivalent to the following:
#include <iostream>
#include <array>
#include <vector>
#include <algorithm>
int main()
{
std::vector<int> arr = { 1, 3, 5, 7, 8 };
std::transform(
std::begin(arr), std::end(arr), std::begin(arr),
[](auto const v) {return v * 2;});
for (auto i = std::begin(arr); i != std::end(arr); ++i)
std::cout << *i << std::endl;
}
Take notice that the Visual C++ compiler only supports punctuation digraphs if compiled with /Za
(aka Disable language extensions), though in practice none of the VC++ compilers that I have seem to support that. On the other hand, you need to include the <iso646.h>
header to have the alternative operator tokens.
There is also a set of alternative tokens of three characters called trigraphs. These are however due to be removed in C++17.
References
Learn more about these topics in these articles:
History
- 2nd October, 2015: Initial version
Marius Bancila is the author of Modern C++ Programming Cookbook and The Modern C++ Challenge. He has been a Microsoft MVP since 2006, initially for VC++ and nowadays for Development technologies. He works as a system architect for Visma, a Norwegian-based company. He works with various technologies, both managed and unmanaged, for desktop, cloud, and mobile, mainly developing with VC++ and VC#. He keeps a blog at http://www.mariusbancila.ro/blog, focused on Windows programming. You can follow Marius on Twitter at @mariusbancila.