3.17.1.1. C++ standard

C++ standard flags should be set globally. You should avoid using any commands that set it locally for target or project.

Note

Example tested with GCC 5.4.1 on Linux. Different compilers may work with C++ standards differently.

Examples on GitHub

3.17.1.1.1. Example

Let’s assume we have header-only library boo implemented by Boo.hpp which can work with both C++98 and C++11:

// Boo.hpp

#ifndef BOO_HPP_
#define BOO_HPP_

#if __cplusplus >= 201103L
# include <thread>
#endif

class Boo {
 public:
#if __cplusplus >= 201103L
  typedef std::thread thread_type;
  static void call(thread_type&) {
  }
#else
  class InternalThread {
  };
  typedef InternalThread thread_type;
  static void call(thread_type&) {
  }
#endif
};

#endif // BOO_HPP_

Library foo that depends on boo and use C++11 internally:

// Foo.hpp

#ifndef FOO_HPP_
#define FOO_HPP_

#include <Boo.hpp>

class Foo {
 public:
  static int optimize(Boo::thread_type&);
};

#endif // FOO_HPP_
// Foo.cpp

#include <Foo.hpp>

constexpr int foo_helper_value() {
  return 0x42;
}

int Foo::optimize(Boo::thread_type&) {
  return foo_helper_value();
}

Executable baz knows nothing about standards and just use API of Boo and Foo classes, Foo is optional:

// main.cpp

#include <iostream> // std::cout

#include <Boo.hpp>

#ifdef WITH_FOO
# include <Foo.hpp>
#endif

int main() {
  Boo::thread_type t;

  std::cout << "C++ standard: " << __cplusplus << std::endl;

#ifdef WITH_FOO
  std::cout << "With Foo support" << std::endl;
  Foo::optimize(t);
#endif

  Boo::call(t);
}

Graphically it will look like this:

Targets

CMake project :

# CMakeLists.txt

cmake_minimum_required(VERSION 3.1)
project(foo)

add_library(boo INTERFACE)
target_include_directories(boo INTERFACE "$<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}>")

add_executable(baz main.cpp)
target_link_libraries(baz PUBLIC boo)

if(WITH_FOO)
  add_library(foo Foo.cpp Foo.hpp)
  target_link_libraries(foo PUBLIC boo)
  target_link_libraries(baz PUBLIC foo)
  target_compile_definitions(baz PUBLIC WITH_FOO)
endif()

Overview:

  • boo provide same API for both C++11 and C++98 configuration so user don’t have to worry about standards.
  • foo use some C++11 feature but only internally.
  • baz don’t know anything about used standards, interested only in boo or foo API.

Imagine that baz for the long time relies only on boo, it’s important to keep this functionality even for old C++98 configuration. But there is foo library that use C++11 and allow us to introduce some optimization.

We want:

  • C++11 with foo
  • C++11 without foo
  • C++98 with foo should produce error message as soon as possible
  • C++98 without foo

3.17.1.1.2. Bad

The first thing that comes to mind after looking at C++ code is that since foo use constexpr feature internally we should do:

# CMakeLists.txt

cmake_minimum_required(VERSION 3.1)
project(foo)

add_library(boo INTERFACE)
target_include_directories(boo INTERFACE "$<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}>")

add_executable(baz main.cpp)
target_link_libraries(baz PUBLIC boo)

if(WITH_FOO)
  add_library(foo Foo.cpp Foo.hpp)
  target_compile_features(foo PRIVATE cxx_constexpr)
  target_link_libraries(foo PUBLIC boo)
  target_link_libraries(baz PUBLIC foo)
  target_compile_definitions(baz PUBLIC WITH_FOO)
endif()

This is not correct and will end with error on link stage after successful generation and compilation:

[examples]> rm -rf _builds
[examples]> cmake -Htoolchain-usage-examples/globals/cxx-standard/bad -B_builds -DWITH_FOO=ON
-- The C compiler identification is GNU 5.4.1
-- The CXX compiler identification is GNU 5.4.1
...
-- Configuring done
-- Generating done
-- Build files have been written to: /.../examples/_builds
[examples]> cmake --build _builds
Scanning dependencies of target foo
[ 25%] Building CXX object CMakeFiles/foo.dir/Foo.cpp.o
[ 50%] Linking CXX static library libfoo.a
[ 50%] Built target foo
Scanning dependencies of target baz
[ 75%] Building CXX object CMakeFiles/baz.dir/main.cpp.o
[100%] Linking CXX executable baz
CMakeFiles/baz.dir/main.cpp.o: In function `main':
main.cpp:(.text+0x64): undefined reference to `Foo::optimize(Boo::InternalThread&)'
collect2: error: ld returned 1 exit status
CMakeFiles/baz.dir/build.make:95: recipe for target 'baz' failed
make[2]: *** [baz] Error 1
CMakeFiles/Makefile2:104: recipe for target 'CMakeFiles/baz.dir/all' failed
make[1]: *** [CMakeFiles/baz.dir/all] Error 2
Makefile:83: recipe for target 'all' failed
make: *** [all] Error 2

The reason is violation of ODR rule, similar example have been described before. Effectively we are having two different libraries boo_11 and boo_98 with the same symbols:

Targets

3.17.1.1.3. Toolchain

Let’s create toolchain file cxx11.cmake instead so we can use it to set standard globally for all targets in project:

# cxx11.cmake

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED YES)

You can add it with -DCMAKE_TOOLCHAIN_FILE=/path/to/cxx11.cmake:

[examples]> rm -rf _builds
[examples]> cmake -Htoolchain-usage-examples/globals/cxx-standard/toolchain -B_builds -DCMAKE_TOOLCHAIN_FILE=cxx11.cmake -DWITH_FOO=YES
-- The C compiler identification is GNU 5.4.1
-- The CXX compiler identification is GNU 5.4.1
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /.../examples/_builds
[examples]> cmake --build _builds
Scanning dependencies of target foo
[ 25%] Building CXX object CMakeFiles/foo.dir/Foo.cpp.o
[ 50%] Linking CXX static library libfoo.a
[ 50%] Built target foo
Scanning dependencies of target baz
[ 75%] Building CXX object CMakeFiles/baz.dir/main.cpp.o
[100%] Linking CXX executable baz
[100%] Built target baz
[examples]> ./_builds/baz
C++ standard: 201103
With Foo support

Looks better now!

3.17.1.1.4. try_compile

The next thing we need to improve is early error detection. Note that now if we try to specify WITH_FOO=ON with C++98 there will be no errors reported on generation stage. Build will failed while trying to compile foo target.

To do this you can create C++ file and add few samples of features you are planning to use:

// features_used_by_foo.cpp

constexpr int value() {
  return 0x42;
}

int main() {
  return value();
}

Use CMake module with try_compile to test this code:

# features_used_by_foo.cmake

set(bindir "${CMAKE_CURRENT_BINARY_DIR}/foo/try_compile")
set(saved_output "${bindir}/output.txt")
set(srcfile "${CMAKE_CURRENT_LIST_DIR}/features_used_by_foo.cpp")
try_compile(
    FOO_IS_FINE
    "${bindir}"
    "${srcfile}"
    OUTPUT_VARIABLE output
)
if(NOT FOO_IS_FINE)
  file(WRITE "${saved_output}" "${output}")
  message(
      FATAL_ERROR
      "Can't compile test file:\n"
      " ${srcfile}\n"
      "Error log:\n"
      " ${saved_output}\n"
      "Please check that your compiler supports C++11 features and C++11 standard enabled."
  )
endif()

Include this check before creating target foo:

# CMakeLists.txt

cmake_minimum_required(VERSION 3.1)
project(foo)

add_library(boo INTERFACE)
target_include_directories(boo INTERFACE "$<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}>")

add_executable(baz main.cpp)
target_link_libraries(baz PUBLIC boo)

if(WITH_FOO)
  include("${CMAKE_CURRENT_LIST_DIR}/features_used_by_foo.cmake")
  add_library(foo Foo.cpp Foo.hpp)
  target_link_libraries(foo PUBLIC boo)
  target_link_libraries(baz PUBLIC foo)
  target_compile_definitions(baz PUBLIC WITH_FOO)
endif()

3.17.1.1.5. Defaults

As usual cache variables allow us to set default values if needed:

# CMakeLists.txt

cmake_minimum_required(VERSION 3.1)

set(
    CMAKE_TOOLCHAIN_FILE
    "${CMAKE_CURRENT_LIST_DIR}/cxx11.cmake"
    CACHE
    FILEPATH
    "Default toolchain"
)

project(foo)

option(WITH_FOO "Enable Foo optimization" ON)

add_library(boo INTERFACE)
target_include_directories(boo INTERFACE "$<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}>")

add_executable(baz main.cpp)
target_link_libraries(baz PUBLIC boo)

if(WITH_FOO)
  include("${CMAKE_CURRENT_LIST_DIR}/features_used_by_foo.cmake")
  add_library(foo Foo.cpp Foo.hpp)
  target_link_libraries(foo PUBLIC boo)
  target_link_libraries(baz PUBLIC foo)
  target_compile_definitions(baz PUBLIC WITH_FOO)
endif()

Note

3.17.1.1.6. Scalability

If this example looks simple and used approach look like an overkill just imagine next situation:

  • boo is external library that supports C++98, C++11, C++14, etc. standards and consists of 1000+ source files
  • foo is external library that supports only few modern standards and tested with C++11 and C++17. Consist of 1000+ source files and non-trivially interacts with boo
  • Your project baz has boo requirement and optional foo, should works correctly in all possibles variations

The worst that may happen if you will use toolchain approach is that foo will fail with compile error instead of error on generation stage. The error will be plain, such as Can't use 'auto', -std=c++11 is missing?. This can be easily improved with try_compile.

If you will keep using locally specified standard like modifying CXX_STANDARD property and conflict will occur:

  • there will be no warning messages on generate step
  • there will be no warning messages on compile step
  • link will fail with opaque error pointing to some implementation details inside boo library while your usage of boo API will look completely fine

When you will try to find error elsewhere:

  • stand-alone version of boo will work correctly with all examples and standards
  • stand-alone version of foo will interact correctly with boo with all examples and supported standards
  • your project baz will work correctly with boo if you will use configuration without foo

3.17.1.1.7. Summary

  • Use toolchain if you need to specify standard, set default toolchain if needed
  • Avoid using CXX_STANDARD in your code
  • Avoid using CMAKE_CXX_STANDARD anywhere except toolchain
  • Avoid using target_compile_features module
  • If you have to use them for any reason at least protect it with if:
if(NOT EXISTS "${CMAKE_TOOLCHAIN_FILE}")
  set_target_properties(boo PROPERTIES CXX_STANDARD 14)
  target_compile_features(foo PUBLIC cxx_constexpr)
endif()