3.18.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.18.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:
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 inboo
orfoo
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 possibleC++98 without
foo
3.18.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:
3.18.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.18.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.18.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
Toolchain should be set before first
project
command, see Project: Tools discovering
See also
3.18.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 filesfoo
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 withboo
Your project
baz
hasboo
requirement and optionalfoo
, 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 ofboo
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 standardsstand-alone version of
foo
will interact correctly withboo
with all examples and supported standardsyour project
baz
will work correctly withboo
if you will use configuration withoutfoo
3.18.1.1.7. Summary¶
Use toolchain if you need to specify standard, set default toolchain if needed
Avoid using
CXX_STANDARD
in your codeAvoid using
CMAKE_CXX_STANDARD
anywhere except toolchainAvoid using
target_compile_features
moduleIf 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()