Jonathan Boccara's blog

Modern C++: 7 Ways to Fake It Until You Have It

Published August 31, 2018 - 6 Comments

Daily C++

Do you wish you had a later version of C++ in your production code? If you do, you’re not alone: a lot of C++ developers today don’t work with a compiler that supports the latest version of the standard.

It could be for many reasons: perhaps you have a lot of legacy code to migrate, or your clients do, or your hardware doesn’t have the adequate infrastructure yet. The point is, you can’t benefit from the latest features that the language offers, and that’s a shame because some of them would surely make your code more expressive.

But even if you can’t use those features, you don’t have to give up on their benefits. At least some of their benefits. There are way you could use the ideas of the new features in your code, to convey your intents more precisely.

Sure enough, it’s not as good as having them natively, which is why updating your compilers is still a necessity. But in the meantime, here are 7 ways to emulate those features, that will improve your code at a minimal cost.

modern C++ fake it until you have it

#1 = default, = delete

In C++11, = default gives an instruction to the compiler to generate either one of:

  • a default constructor,
  • a copy constructor,
  • a copy assignment operator,
  • a move constructor,
  • a move assignment operator,
  • or a destructor.

In some cases the compiler would generate those functions anyway. But with C++11, some developers like to express this in their interfaces, to reassure a reader that they’re aware of the automatic generation of those methods, and that this is what they wanted for the class.

Before C++11, there wasn’t a way to express this natively. But nothing prevents you from writing this in a comment:

class X
{
    /* X(const X& other) = default; */
    /* X& operator=(const X& other) = default; */
    /* ~X() = default;*/
    
    // rest of X ...
};

Simlarly, to prevent the compiler from generating those functions, before C++11 we had to declare them private and not implement them:

class X
{
    // rest of X ...

private:
    X(const X& other);
    X& operator=(const X& other);
};

In C++11 we’d put those public and tack on an = delete to disable the compiler generation of those functions.

Before C++11 we can be more explicit than just putting them private, by tacking on an = delete (but not a real one, a comment one):

class X
{
    // rest of X ...

private:
    X(const X& other) /* = delete */;
    X& operator=(const X& other) /* = delete */;
};

#2 Standard algorithms

The useful STL algorithms library is growing with new algorithms along with the new versions of C++. Some of those algorithms are very generic. Take the example of copy_if, or all_of and its siblings any_of and none_of.

Surprising as it sounds, they didn’t get into the standard before C++11.

But getting access to them for a pre-C++11 codebase is very simple: just visit a reference website such as cppreference.com, grab their implementation (here is copy_if and here is all_of and siblings, for instance), put it into your code, and off you go. The whole operation takes some 10 seconds, and will save you a lot more time by using them in your code.

#3 Attributes

Attributes are the keywords that are between double pair of brackets: [[example_attribute]]. They start to appear in C++11, and their number is augmented in C++17. For a in-depth analysis of attributes, have a look at Bartek’s C++17 is details: Attributes, but the general idea of attributes is that you can use them as markers in your code, to express your intentions both to other humans reading your code, and to the compiler.

Take the example of the [[fallthrough]] attribute. This attribute is used in switch statements, when you purposefully don’t put a break in one of the cases, in order to execute its code AND the code of the next following case:

switch (myValue)
{
    case value1:
    {
        // do something
        break;
    }
    case value2:
    {
        // do something
    }
    case value3:
    {
        // do something
        break;
    }
}

Notice how case value2 doesn’t have a break instruction? This is worrying because it looks like a bug. Most of the time it is, except in some cases where you really want to execute both cases for value2. [[fallthrough]] lets you make this clear for everyone:

switch (myValue)
{
    case value1:
    {
        // do something
        break;
    }
    case value2:
    {
        // do something
        [[fallthrough]];
    }
    case value3:
    {
        // do something
        break;
    }
}

It prevents any warning from the compiler, and it shows other developers that you knew what you were doing when you wrote that piece of code.

Before C++17, if you tend to use this technique of omitting the break you wouldn’t have the warning active anyway, but you can at least express this intention to your fellow developers by making [[fallthrough]] appear somehow:

switch (myValue)
{
    case value1:
    {
        // do something
        break;
    }
    case value2:
    {
        // do something
        //[[fallthrough]];
    }
    case value3:
    {
        // do something
        break;
    }
}

The same goes for the other attributes brought by C++11 and C++17.

#4 Concepts

Concepts are a very expected feature for C++, that should normally be part of C++20. A concept is essentially an interface, for templates. Concepts allow to write something more precise than typename to define template parameters. Indeed, typename only means “this is a type”, but doesn’t say anything else about that type.

A concept like Iterator for example should replace typename in template code that manipulates iterators. And Iterator would be defined as having specific operations (incrementing, dereferencing). Passing a type that doesn’t have those specific operations would fail to compile with a clear error message, that would explain why this type is not an Iterator as expected.

I’m not going show you how to emulate concepts themselves before they get into the language. This is a pretty tricky thing to do and if you’d like to see this, you can have a look at the implementation of range-v3 that uses pretty advanced techniques to emulate this feature.

No, the much easier piece of advice I will recommend is to choose template parameters names with care, and use concepts names for them when possible. Even if you can’t replace the word typename before having concepts, you have a total liberty when it comes to choosing the name of the type parameter.

To pick up the example of the iterator, don’t call your template parameters typename T or typename I, but rather typename Iterator in this case. We’d never call an int i just because it’s an int, but we tend to do it more for template types.

The name of a template type is all over the place in template code, so let’s give it a good name, and use standard names of concepts that are being worked on now. Using them should make our code fall into place when actual concepts come into the language (and into our codebases).

#5 Ranges algorithms

The STL is a fantastic library, but there is something cumbersome to use with it: iterators. Indeed, every STL algorithm takes two iterators to define an input range to work with.

This is useful when you need to apply an algorithm on a subpart of your range, but when you need to traverse a whole range (which is the most common case anyway), iterators get in the way:

auto positionOf42 = std::find(begin(myCollection), end(myCollection), 42);

It would be much simpler to be able to pass the range as a whole:

auto positionOf42 = std::find(myCollection, 42);

This is what the ranges proposal aims at doing in C++20 (amongst plenty other things). But this part is very easy to emulate even in C++98, by wrapping calls to STL algorithms into functions that take a range:

template<typename Range, typename Value>
typename Range::iterator find(Range& range, Value const& value)
{
    return std::find(begin(range), end(range), value);
}

template<typename Range, typename Value>
typename Range::const_iterator find(Range const& range, Value const& value)
{
    return std::find(begin(range), end(range), value);
}

#6 Libraries that emulate standard components

Some standard library components are more complex than algorithm wrappers to implement, and require more work to emulate for your codebase.

Take the example of std::optional, or std::variant for example, which entered the language in C++17. If you don’t have C++17, it can be challenging to write your own implementations, that faithfully replicate the interface of the the standard one, and that is as thoroughly tested.

Fortunately, there is no need to make this effort, because someone else has made it for you already.

The next to standard library is Boost. Some components, including optionalvariant and some of the more recent STL algorithms have originated there. However, note that interface of Boost library can evolve, because Boost is more concerned with pushing the limits of the language than preserving backwards compatibility at all costs.

Moreover, some standard components have some subtle differences with their Boost counterparts. For example, boost::optional accepts reference types, while std::optional doesn’t. So std::optional is not a drop-in replacement for all cases.

Other libraries provide C++11 implementations of C++17 standard components, such as Google’s Abseil for example. Abseil’s website announces that “Google has developed many abstractions that either match or closely match features incorporated into C++14, C++17, and beyond. Using the Abseil versions of these abstractions allows you to access these features now, even if your code is not yet ready for life in a post C++11 world.”

In their source code, we can indeed see that some components resolve to aliases to the standard ones if those are available.

#7 Metaclasses

This is probably the most distant proposal in time, but also one of the most popular in the C++ community. Metaclasses allow to define classes at compile time, and enrich the ways to define a type beyond struct and class.

One of the canonical examples of the proposal is the interface metaclass, that would allow to declare the methods of an interface with the keyword interface, and leave the compiler worry about writing the virtual destructor, making methods pure virtual, making sure there is no data nor private members, in a word everything that characterises an interface.

The code would look like this:

interface Player
{
    void play();
    void pause();
    void stop();
};

In contrast, today we’d write such an interface this way:

class Player
{
public:
    virtual void play() = 0;
    virtual void pause() = 0;
    virtual void stop() = 0;
    virtual ~Player() = 0;
};

There is not much we can do to emulate metaclass today, but why not specifying that we mean a class to be like an interface metaclass, by making it appear somehow?

class /* interface */ Player
{
public:
    virtual void play() = 0;
    virtual void pause() = 0;
    virtual void stop() = 0;
    virtual ~Player() = 0;
};

It doesn’t cost anything, but gives a hint on your intention for the next person reading your code. And this would be true also for the other proposed metaclasses.

You’ll have to upgrade one day

Those 7 tips will instantly bring you some of the benefits of Modern (and even post-Modern) C++ today, for a minimal cost. For much less than upgrading your compilers, at least. They also give you some practice and familiarity with the future versions of the language.

But this doesn’t mean that you should stop here. This is just a taste of Modernity, and the language is evolving every three years. If you don’t want to lag behind, you need to upgrade your compilers, and then emulate the latest features, and then upgrade to them again, and then emulate the new latest features, and so on, and so forth.

This is a never-ending race to modern code, and we are many to run together.

Don't want to miss out ? Follow:   twitterlinkedinrss
Share this post!Facebooktwitterlinkedin

Comments are closed