Morten's Dev

C++, Python, Rust, Emacs, Clang, CMake, and other technobabble..

Sigs: Migrating to C++17


sigs has been migrated to using C++17 from C++14. Didn’t take much doing other than some CMake configuration tweakings, and to update the Google Test framework to version 1.8.1.

1. CMake configuration changes

Setting the required C++ standard to C++17 was done with:

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

And was tested to work with Clang 6 + 7, GCC 7 + 8, and VS 2017.

2. C++17 features

The fun part was finding new features to put into action in the project.

2.1. [[nodiscard]]

In certain cases it is important that the returned value is not discarded, which is exactly what [[nodiscard]] achieves. For example, in the following a warning will be shown if the return value of Signal::interface() isn’t used:

[[nodiscard]] inline std::unique_ptr<Interface> interface() noexcept
{
  return std::make_unique<Interface>(this);
}

2.2. Initializer statements

Having the means to include initialization in if and switch statements is a very nice, new feature that helps scoping variables for intent. If an initialization is made before the statement then it will be live in the parent scope of that statement, a scope that might span for more than what was intended.

There were no places in sigs where a scope of an initializer to an if or switch statement spanned more than was necessary, but still, there were four places to put it into effect. An example:

if (auto *sig = entry.signal(); sig) {
  (*sig)(retFunc, std::forward<Args>(args)...);
}

2.3. std::optional

The concept of an optional value is a nice abstraction that enables one to not use any value, from the return value space, to act as an indicator of null or an error. For instance, if the return type of a function is int then an error might be signaled by using -1, but that also means that the value cannot be used as a valid value. Instead, std::optional<T> can be seen as a bool for indicating an error and the encapsulated value T if defined.

An example usage from the project:

template <typename Ret, typename... Args>
class Signal<Ret(Args...)> final {
public:
  // ..

  void disconnect(std::optional<Connection> conn) noexcept
  {
    // ..
  }

// ..

This solution is clearer and better than the previous one:

void disconnect(Connection conn = nullptr)

2.4. std::is_void_v

std::is_void was introduced already in C++11 and std::is_void_v is basically just a helper variable template:

template <typename T>
inline constexpr bool is_void_v = is_void<T>::value;

In sigs, I ended up replacing the local isVoidReturn() with direct usage due to the shortness of the statement:

static_assert(!std::is_void_v<ReturnType>, "Must have non-void return type!");

2.5. noexcept

Every function in C++ is either potentially throwing or non-throwing. noexcept is interesting because it marks a function as non-throwing, and informs that exceptions aren’t being used intentionally. If an exception is thrown anyway, std::terminate is invoked.

noexcept was added in C++11 but I didn’t put it to use until now. It made it possible for the compiler to optimize further and reduced the size of the binaries somewhat. Can’t say no to that. I found 34 places to add it, like:

Signal() noexcept = default;

Related Posts