Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++

C++ is Fun: Tips and Tricks

4.94/5 (61 votes)
2 Oct 2015CPOL6 min read 52.6K  
A walk-through several less known C++ features

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.

C++
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:

C++
bool foobar()
try
{
   // execute something and return true if successful
   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:

C++
bool foobar()
{
   try
   {
      // execute something and return true if successful
      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.

C++
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:

C++
foo constructed
error in bar!

To correctly destroy the foo object, you must try-catch the constructor initializer list.

C++
foobar()
  try
  : m_foo(new foo()),
    m_bar()
{
  std::cout << "foobar constructed" << std::endl;
}
catch (...)
{
  delete m_foo;
  throw;
}

The new program will print:

C++
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:

C++
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.

C++
namespace 
{
   void print(std::string message) {}
}

The above namespace is the equivalent to the following code:

C++
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.

C++
// foo.cpp
void print(std::string message)
{
   std::cout << "[foo]" << message << std::endl;
}

// bar.cpp
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.

C++
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.

C++
struct base
{
   virtual void run() = 0;
}

It is little known though that a pure virtual function can actually have a body:

C++
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:
C++
struct C {
virtual void f() = 0 { }; // ill-formed
};

—end example ]

C++
struct base
{
   virtual void run() = 0 // this is OK only in VC++
   {
      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:

  • If two derived classes share common code, that code can be put in the body of the pure virtual function in the base class. This doesn’t bring anything new than non-pure virtual function, however, the pure specification forces the derived classes to implement the function, which is not the case with non-pure virtual functions.
    C++
    struct foo : base
    {
       virtual void run() override
       {
          base::run();
          std::cout << "foo::run" << std::endl;
       }
    };
    
    struct bar : base
    {
       virtual void run() override
       {
          base::run();
          std::cout << "bar::run" << std::endl;
       }
    };
  • A base class should have its destructor virtual. If the class should be abstract but it does not have any virtual functions, then the virtual destructor can be made pure. On the other hand, a derived class destructor must call the base class destructor, so the pure virtual destructor of the base class must have an implementation.

lvalue Conditional Operator

The conditional operator allows you to write shorter conditional code.

C++
auto a = 12;
auto b = 42;

auto max = a >= b ? a : b;

That is basically equivalent to:

C++
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++
c % 2 == 0 ? a : b = 1;

And you can very well have something like this:

C++
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:

C++
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).

C++
%: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:

C++
#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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)