├── .clang-format
├── .cppcheck-suppressions
├── .gitignore
├── .vscode
├── c_cpp_properties.json
└── settings.json
├── CMakeLists.txt
├── LICENSE
├── README.md
├── benchmark
├── CMakeLists.txt
└── rtti_benchmark.cc
├── cmake
├── clang-format.cmake
├── code-coverage.cmake
├── googlebenchmark.cmake
└── googletest.cmake
├── include
├── hash.hh
└── rtti.hh
└── tests
├── CMakeLists.txt
├── hierarchy_test.cc
└── typeinfo_test.cc
/.clang-format:
--------------------------------------------------------------------------------
1 | ---
2 | BasedOnStyle: Google
3 |
4 | IndentCaseLabels: true
5 | IndentWidth: 4
6 | NamespaceIndentation: All
7 | TabWidth: 4
8 | ColumnLimit: 100
9 |
10 | AccessModifierOffset: -4
11 |
12 | BreakConstructorInitializers: BeforeComma
13 | BreakInheritanceList: BeforeComma
14 | BreakBeforeBraces: Custom
15 | BraceWrapping:
16 | AfterStruct: false
17 | AfterClass: false
18 | AfterEnum: false
19 |
20 | AlwaysBreakAfterReturnType: TopLevel
21 | AllowShortFunctionsOnASingleLine: Empty
22 | AllowShortLambdasOnASingleLine: Empty
23 | AllowShortCaseLabelsOnASingleLine: true
24 | AllowShortIfStatementsOnASingleLine: Never
25 | AllowShortBlocksOnASingleLine: false
26 | AllowShortLoopsOnASingleLine: false
27 | AlwaysBreakTemplateDeclarations: Yes
28 | IndentPPDirectives: BeforeHash
29 |
30 | FixNamespaceComments: true
31 |
32 | PointerAlignment: Left
33 | AlignEscapedNewlines: Left
34 | AlignConsecutiveMacros: true
35 | AlignTrailingComments: true
36 |
--------------------------------------------------------------------------------
/.cppcheck-suppressions:
--------------------------------------------------------------------------------
1 | *:*/build/external/*
2 | unreadVariable:*/tests/*
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | build
--------------------------------------------------------------------------------
/.vscode/c_cpp_properties.json:
--------------------------------------------------------------------------------
1 | {
2 | "configurations": [
3 | {
4 | "name": "Linux",
5 | "cStandard": "c11",
6 | "cppStandard": "c++17",
7 | "includePath": [
8 | "${workspaceFolder}/include/**",
9 | "${workspaceFolder}/build/external/include/**"
10 | ]
11 | }
12 | ],
13 | "version": 4
14 | }
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cmake.configureOnOpen": false,
3 | "files.associations": {
4 | "type_traits": "cpp"
5 | }
6 | }
--------------------------------------------------------------------------------
/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required(VERSION 3.10)
2 | project(rtti CXX)
3 |
4 | set(CMAKE_COLOR_MAKEFILE ON)
5 | set(CMAKE_VERBOSE_MAKEFILE OFF)
6 |
7 | # Build options
8 | option(ENABLE_DOCS "Enable documentation target" OFF)
9 | option(ENABLE_CODE_COVERAGE "Enable code coverage generation" OFF)
10 | option(ENABLE_CODE_ANALYSIS "Enable static code analysis" OFF)
11 | option(ENABLE_CPPCHECK "Enable cppcheck for static code analysis" OFF)
12 | option(ENABLE_CLANG_TIDY "Enable clang-tidy for static code analysis" OFF)
13 | option(BUILD_TESTS "Build unittest binary" ON)
14 | option(BUILD_BENCHMARK "Build benchmark binary" ON)
15 |
16 | # CXX compilation options
17 | set(CMAKE_CXX_STANDARD 17)
18 | set(CMAKE_CXX_STANDARD_REQUIRED ON)
19 | set(CMAKE_CXX_EXTENSIONS OFF)
20 | set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS} -O0 -ggdb")
21 | set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS} -O3")
22 |
23 | # CMake module path
24 | list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake)
25 |
26 | # Vendor installation path
27 | set(EXTERNAL_INSTALL_DIR ${PROJECT_BINARY_DIR}/external)
28 | include_directories(${EXTERNAL_INSTALL_DIR}/include)
29 | link_directories(${EXTERNAL_INSTALL_DIR}/lib)
30 |
31 | # Code formatting
32 | include(clang-format)
33 |
34 | # Code coverage generation
35 | if(ENABLE_CODE_COVERAGE)
36 | include(code-coverage)
37 | append_coverage_compiler_flags()
38 | endif()
39 |
40 | # Static code analysis
41 | if(ENABLE_CODE_ANALYSIS)
42 | if (ENABLE_CPPCHECK)
43 | message(STATUS "Attempting to find code analysis tools:")
44 | find_program(CMAKE_CXX_CPPCHECK NAMES cppcheck)
45 | if(CMAKE_CXX_CPPCHECK)
46 | message(STATUS "- Found `cppcheck`")
47 | list(APPEND CMAKE_CXX_CPPCHECK
48 | "--enable=all"
49 | "--suppress=missingIncludeSystem"
50 | "--inline-suppr"
51 | "--force"
52 | "--suppressions-list=${CMAKE_SOURCE_DIR}/.cppcheck-suppressions"
53 | )
54 | endif()
55 | endif()
56 |
57 | if (ENABLE_CLANG_TIDY)
58 | find_program(CMAKE_CXX_CLANG_TIDY NAMES clang-tidy)
59 | if(CMAKE_CXX_CPPCHECK)
60 | message(STATUS "- Found `clang-tidy`")
61 | list(APPEND CMAKE_CXX_CLANG_TIDY
62 | "-header-filter=${CMAKE_SOURCE_DIR}/include"
63 | "-checks=*"
64 | )
65 | endif()
66 | endif()
67 | endif()
68 |
69 | # Library
70 | set(SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/include/rtti.hh)
71 | add_library(rtti INTERFACE)
72 | target_include_directories(rtti INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include)
73 | clang_format(rtti ${SOURCES})
74 |
75 | # Configure install targets
76 | include(GNUInstallDirs)
77 | install(DIRECTORY include/
78 | DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
79 | FILES_MATCHING PATTERN "*.hh"
80 | )
81 |
82 | # Documentation
83 | if (ENABLE_DOCS)
84 | find_package(Doxygen REQUIRED dot mscgen dia)
85 |
86 | set(DOXYGEN_GENERATE_HTML YES)
87 | set(DOXYGEN_GENERATE_MAN NO)
88 |
89 | doxygen_add_docs(docs
90 | ${CMAKE_CURRENT_SOURCE_DIR}/include
91 | COMMENT "Generate documentation")
92 | endif()
93 |
94 | # Testing
95 | if (BUILD_TESTS)
96 | enable_testing()
97 | include(googletest)
98 | add_subdirectory(tests)
99 | endif()
100 |
101 | # Benchmark
102 | if (BUILD_BENCHMARK)
103 | include(googlebenchmark)
104 | add_subdirectory(benchmark)
105 | endif()
106 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Roy van Dam
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Open-hierarchy custom RTTI framework for C++17 and up.
2 |
3 | If you have ever attempted to use the C++'s build in RTTI on a resource constrained (embedded) system you will most likely have noticed it is massively inefficient. Hence this implementation of a hand-rolled form of RTTI which is much more efficient and flexible, although it requires a bit more work from you as a class author. The current implementation supports the following features:
4 |
5 | - Compiletime (stable) ID generation based on the FNV1a hash of the type signature
6 | - Multiple inheritance, including virtual
7 | - Full dynamic casting support
8 | - Parent constructors are accessible
9 | - No external dependencies, single header
10 | - Static asserts on the parents passed to TypeInfo structure.
11 | - Works on bare-metal systems
12 | - Currently supports GCC/Clang based compilers
13 | - One convenience marco ¯\\_(ツ)_/¯
14 |
15 | Note: This project was initially inspired by open-hierarchy examples in the guidelines defined for RTTI by the LLVM project[1]. However this solution has one major drawback which is that the parent constructors are no longer accessible given that RTTI classes are injected in between the parent and child. An initial implementation of the RTTI library was based on this design and is still available for reference under git tag `llvm-style-inheritance`.
16 |
17 | [1] https://llvm.org/docs/HowToSetUpLLVMStyleRTTI.html
18 |
19 | ---
20 |
21 | ## How to use
22 |
23 | - Add `-fno-rtti` to your compile options in order to disable C++'s build in RTTI. (Optional, as it can work in conjunction with native RTTI)
24 | - `RTTI::Enable` describes the abstract interface for performing runtime RTTI checks and type casing. It is to be virtually derived from by the highest member(s) in the class hierarchy.
25 | - For each type part of the hierarchy the `RTTI_DECLARE_TYPEINFO(Type, Parents...)` macro should be added after the opening brace to define a type alias to `RTTI::TypeInfo` structure and overload the virtual methods of the interface described by `RTTI::Enable`.
26 | - `RTTI::TypeInfo` holds the type information of each member and provides statis methods for performing RTTI checks and type casting. It uses the “Curiously Recurring Template Idiom”, taking the class being defined as its first template argument and optionally the parent classes as the arguments there after.
27 |
28 | ### Basic example:
29 |
30 | ```c++
31 | struct Shape : virtual RTTI::Enable {
32 | RTTI_DECLARE_TYPEINFO(Shape);
33 | };
34 |
35 | struct Square : Shape {
36 | RTTI_DECLARE_TYPEINFO(Square, Shape);
37 | };
38 |
39 | struct OtherParent : virtual RTTI::Enable {
40 | RTTI_DECLARE_TYPEINFO(OtherParent);
41 | }
42 |
43 | struct Circle : Shape, OtherParent {
44 | RTTI_DECLARE_TYPEINFO(Circle, Shape, OtherParent);
45 | };
46 |
47 | int main() {
48 | Circle c;
49 | Shape* shape = &c;
50 |
51 | if (shape->is()) {
52 | std::cout << "Yes, the shape is a circle!" << std::endl;
53 | }
54 |
55 | if (shape->cast() == nullptr) {
56 | std::cout << "No, it was not a square... :(" << std::endl;
57 | }
58 |
59 | if (auto circle = shape->cast()) {
60 | std::cout << "Woot, we have the circle back! \\0/" << std::endl;
61 | }
62 |
63 | OtherParent *p = &c;
64 | if (auto s = p->cast()) {
65 | std::cout << "Pointer offsets are take into account for multiple inheritance hierarchies." << std::endl;
66 | }
67 |
68 | return 0;
69 | }
70 |
71 | ```
72 |
73 | Note that the `RTTI::TypeInfo::Id()` method can also be used to identify any other types not part of an RTTI hierarchy, for example a very basic interface and implementation of a variant type:
74 |
75 | ```c++
76 | struct AnyVariant {
77 | virtual ~AnyVariant() {}
78 | virtual RTTI::TypeId valueTypeId() const noexcept =0;
79 | };
80 |
81 | template
82 | struct Variant : AnyVariant {
83 | T value;
84 |
85 | virtual RTTI::TypeId valueTypeId() const noexcept override {
86 | return RTTI::TypeInfo::Id();
87 | }
88 | };
89 |
90 | void testValueTypeId() {
91 | Variant v;
92 | assert(v.valueTypeId() == TypeInfo::Id());
93 | assert(v.valueTypeId() != TypeInfo::Id());
94 | }
95 | ```
96 |
97 | ## Benchmark Results
98 |
99 | ```
100 | Running ./build/benchmark/rtti-benchmark
101 | Run on (6 X 4300 MHz CPU s)
102 | CPU Caches:
103 | L1 Data 32 KiB (x6)
104 | L1 Instruction 32 KiB (x6)
105 | L2 Unified 256 KiB (x6)
106 | L3 Unified 9216 KiB (x1)
107 | Load Average: 0.48, 0.26, 0.17
108 | ------------------------------------------------------------
109 | Benchmark Time CPU Iterations
110 | ------------------------------------------------------------
111 | NativeDynamicCast 133 ns 133 ns 5252735
112 | RttiDynamicCast 6.08 ns 6.08 ns 114135771
113 | ```
114 |
115 | ## Contribute
116 |
117 | Have you found a bug/mistake or any other proposal and want to contribute? Feel free to open an issue or pull request!
118 | Happy coding!
119 |
--------------------------------------------------------------------------------
/benchmark/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | add_executable(rtti-benchmark ${CMAKE_CURRENT_SOURCE_DIR}/rtti_benchmark.cc)
2 | target_link_libraries(rtti-benchmark PUBLIC rtti benchmark pthread)
3 | add_dependencies(rtti-benchmark googlebenchmark-external)
4 |
5 | clang_format(rtti-benchtest ${CMAKE_CURRENT_SOURCE_DIR}/rtti_benchmark.cc)
--------------------------------------------------------------------------------
/benchmark/rtti_benchmark.cc:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | #include
4 |
5 | struct GrandParent : virtual RTTI::Enable {
6 | RTTI_DECLARE_TYPEINFO(GrandParent);
7 | };
8 |
9 | struct ParentA : virtual GrandParent {
10 | RTTI_DECLARE_TYPEINFO(ParentA, GrandParent);
11 | };
12 |
13 | struct ParentB : virtual GrandParent {
14 | RTTI_DECLARE_TYPEINFO(ParentB, GrandParent);
15 | };
16 |
17 | struct Child
18 | : ParentA
19 | , ParentB {
20 | RTTI_DECLARE_TYPEINFO(Child, ParentA, ParentB);
21 | };
22 |
23 | struct InvalidCast {};
24 |
25 | static void
26 | NativeDynamicCast(benchmark::State& state) {
27 | for (auto _ : state) {
28 | Child c;
29 |
30 | // Upcasting
31 | GrandParent* pg;
32 | benchmark::DoNotOptimize(pg = dynamic_cast(&c));
33 |
34 | ParentA* pa;
35 | benchmark::DoNotOptimize(pa = dynamic_cast(&c));
36 |
37 | ParentB* pb;
38 | benchmark::DoNotOptimize(pb = dynamic_cast(&c));
39 |
40 | InvalidCast* invalid;
41 | benchmark::DoNotOptimize(invalid = dynamic_cast(&c));
42 |
43 | // Downcasting
44 | Child* pc;
45 | benchmark::DoNotOptimize(pc = dynamic_cast(pg));
46 | benchmark::DoNotOptimize(pc = dynamic_cast(pa));
47 | benchmark::DoNotOptimize(pc = dynamic_cast(pb));
48 | }
49 | }
50 | BENCHMARK(NativeDynamicCast);
51 |
52 | static void
53 | RttiDynamicCast(benchmark::State& state) {
54 | for (auto _ : state) {
55 | Child c;
56 |
57 | // Upcasting
58 | GrandParent* pg;
59 | benchmark::DoNotOptimize(pg = c.cast());
60 |
61 | ParentA* pa;
62 | benchmark::DoNotOptimize(pa = c.cast());
63 |
64 | ParentB* pb;
65 | benchmark::DoNotOptimize(pb = c.cast());
66 |
67 | InvalidCast* invalid;
68 | benchmark::DoNotOptimize(invalid = c.cast());
69 |
70 | // Downcasting
71 | Child* pc;
72 | benchmark::DoNotOptimize(pc = pg->cast());
73 | benchmark::DoNotOptimize(pc = pa->cast());
74 | benchmark::DoNotOptimize(pc = pb->cast());
75 | }
76 | }
77 | BENCHMARK(RttiDynamicCast);
78 |
79 | BENCHMARK_MAIN();
--------------------------------------------------------------------------------
/cmake/clang-format.cmake:
--------------------------------------------------------------------------------
1 | if (NOT CLANGFORMAT_CONFIGURED)
2 | set(CLANGFORMAT_CONFIGURED true)
3 |
4 | set(CLANGFORMAT_CONFIG ${PROJECT_SOURCE_DIR}/.clang-format)
5 | if(EXISTS ${CLANGFORMAT_CONFIG})
6 | set(CLANGFORMAT_STYLE -style=file)
7 | else()
8 | message("Unable to find .clang-format file, using google style")
9 | set(CLANGFORMAT_STYLE -style=google)
10 | endif()
11 |
12 | find_program(CLANGFORMAT clang-format)
13 | if(CLANGFORMAT)
14 | add_custom_target(format)
15 |
16 | function(clang_format TARGET)
17 | set(SOURCES ${ARGN})
18 |
19 | add_custom_target(format-${TARGET}
20 | COMMAND
21 | ${CLANGFORMAT} ${CLANGFORMAT_STYLE} -i
22 | ${SOURCES}
23 | COMMENT
24 | "Formatting sources..."
25 | )
26 |
27 | add_dependencies(format format-${TARGET})
28 | endfunction(clang_format)
29 | else()
30 | message(WARNING "Unable to find clang-format binary")
31 | function(clang_format TARGET)
32 | endfunction(clang_format)
33 | endif()
34 | endif()
35 |
36 |
--------------------------------------------------------------------------------
/cmake/code-coverage.cmake:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2012 - 2017, Lars Bilke
2 | # All rights reserved.
3 | #
4 | # Redistribution and use in source and binary forms, with or without modification,
5 | # are permitted provided that the following conditions are met:
6 | #
7 | # 1. Redistributions of source code must retain the above copyright notice, this
8 | # list of conditions and the following disclaimer.
9 | #
10 | # 2. Redistributions in binary form must reproduce the above copyright notice,
11 | # this list of conditions and the following disclaimer in the documentation
12 | # and/or other materials provided with the distribution.
13 | #
14 | # 3. Neither the name of the copyright holder nor the names of its contributors
15 | # may be used to endorse or promote products derived from this software without
16 | # specific prior written permission.
17 | #
18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
22 | # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25 | # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 | #
29 | # CHANGES:
30 | #
31 | # 2012-01-31, Lars Bilke
32 | # - Enable Code Coverage
33 | #
34 | # 2013-09-17, Joakim Söderberg
35 | # - Added support for Clang.
36 | # - Some additional usage instructions.
37 | #
38 | # 2016-02-03, Lars Bilke
39 | # - Refactored functions to use named parameters
40 | #
41 | # 2017-06-02, Lars Bilke
42 | # - Merged with modified version from github.com/ufz/ogs
43 | #
44 | # 2019-05-06, Anatolii Kurotych
45 | # - Remove unnecessary --coverage flag
46 | #
47 | # 2019-12-13, FeRD (Frank Dana)
48 | # - Deprecate COVERAGE_LCOVR_EXCLUDES and COVERAGE_GCOVR_EXCLUDES lists in favor
49 | # of tool-agnostic COVERAGE_EXCLUDES variable, or EXCLUDE setup arguments.
50 | # - CMake 3.4+: All excludes can be specified relative to BASE_DIRECTORY
51 | # - All setup functions: accept BASE_DIRECTORY, EXCLUDE list
52 | # - Set lcov basedir with -b argument
53 | # - Add automatic --demangle-cpp in lcovr, if 'c++filt' is available (can be
54 | # overridden with NO_DEMANGLE option in setup_target_for_coverage_lcovr().)
55 | # - Delete output dir, .info file on 'make clean'
56 | # - Remove Python detection, since version mismatches will break gcovr
57 | # - Minor cleanup (lowercase function names, update examples...)
58 | #
59 | # 2019-12-19, FeRD (Frank Dana)
60 | # - Rename Lcov outputs, make filtered file canonical, fix cleanup for targets
61 | #
62 | # 2020-01-19, Bob Apthorpe
63 | # - Added gfortran support
64 | #
65 | # 2020-02-17, FeRD (Frank Dana)
66 | # - Make all add_custom_target()s VERBATIM to auto-escape wildcard characters
67 | # in EXCLUDEs, and remove manual escaping from gcovr targets
68 | #
69 | # USAGE:
70 | #
71 | # 1. Copy this file into your cmake modules path.
72 | #
73 | # 2. Add the following line to your CMakeLists.txt:
74 | # include(CodeCoverage)
75 | #
76 | # 3. Append necessary compiler flags:
77 | # append_coverage_compiler_flags()
78 | #
79 | # 3.a (OPTIONAL) Set appropriate optimization flags, e.g. -O0, -O1 or -Og
80 | #
81 | # 4. If you need to exclude additional directories from the report, specify them
82 | # using full paths in the COVERAGE_EXCLUDES variable before calling
83 | # setup_target_for_coverage_*().
84 | # Example:
85 | # set(COVERAGE_EXCLUDES
86 | # '${PROJECT_SOURCE_DIR}/src/dir1/*'
87 | # '/path/to/my/src/dir2/*')
88 | # Or, use the EXCLUDE argument to setup_target_for_coverage_*().
89 | # Example:
90 | # setup_target_for_coverage_lcov(
91 | # NAME coverage
92 | # EXECUTABLE testrunner
93 | # EXCLUDE "${PROJECT_SOURCE_DIR}/src/dir1/*" "/path/to/my/src/dir2/*")
94 | #
95 | # 4.a NOTE: With CMake 3.4+, COVERAGE_EXCLUDES or EXCLUDE can also be set
96 | # relative to the BASE_DIRECTORY (default: PROJECT_SOURCE_DIR)
97 | # Example:
98 | # set(COVERAGE_EXCLUDES "dir1/*")
99 | # setup_target_for_coverage_gcovr_html(
100 | # NAME coverage
101 | # EXECUTABLE testrunner
102 | # BASE_DIRECTORY "${PROJECT_SOURCE_DIR}/src"
103 | # EXCLUDE "dir2/*")
104 | #
105 | # 5. Use the functions described below to create a custom make target which
106 | # runs your test executable and produces a code coverage report.
107 | #
108 | # 6. Build a Debug build:
109 | # cmake -DCMAKE_BUILD_TYPE=Debug ..
110 | # make
111 | # make my_coverage_target
112 | #
113 |
114 | include(CMakeParseArguments)
115 |
116 | # Check prereqs
117 | find_program( GCOV_PATH gcov )
118 | find_program( LCOV_PATH NAMES lcov lcov.bat lcov.exe lcov.perl)
119 | find_program( GENHTML_PATH NAMES genhtml genhtml.perl genhtml.bat )
120 | find_program( GCOVR_PATH gcovr PATHS ${CMAKE_SOURCE_DIR}/scripts/test)
121 | find_program( CPPFILT_PATH NAMES c++filt )
122 |
123 | if(NOT GCOV_PATH)
124 | message(FATAL_ERROR "gcov not found! Aborting...")
125 | endif() # NOT GCOV_PATH
126 |
127 | if("${CMAKE_CXX_COMPILER_ID}" MATCHES "(Apple)?[Cc]lang")
128 | if("${CMAKE_CXX_COMPILER_VERSION}" VERSION_LESS 3)
129 | message(FATAL_ERROR "Clang version must be 3.0.0 or greater! Aborting...")
130 | endif()
131 | elseif(NOT CMAKE_COMPILER_IS_GNUCXX)
132 | if("${CMAKE_Fortran_COMPILER_ID}" MATCHES "[Ff]lang")
133 | # Do nothing; exit conditional without error if true
134 | elseif("${CMAKE_Fortran_COMPILER_ID}" MATCHES "GNU")
135 | # Do nothing; exit conditional without error if true
136 | else()
137 | message(FATAL_ERROR "Compiler is not GNU gcc! Aborting...")
138 | endif()
139 | endif()
140 |
141 | set(COVERAGE_COMPILER_FLAGS "-g -fprofile-arcs -ftest-coverage"
142 | CACHE INTERNAL "")
143 |
144 | set(CMAKE_Fortran_FLAGS_COVERAGE
145 | ${COVERAGE_COMPILER_FLAGS}
146 | CACHE STRING "Flags used by the Fortran compiler during coverage builds."
147 | FORCE )
148 | set(CMAKE_CXX_FLAGS_COVERAGE
149 | ${COVERAGE_COMPILER_FLAGS}
150 | CACHE STRING "Flags used by the C++ compiler during coverage builds."
151 | FORCE )
152 | set(CMAKE_C_FLAGS_COVERAGE
153 | ${COVERAGE_COMPILER_FLAGS}
154 | CACHE STRING "Flags used by the C compiler during coverage builds."
155 | FORCE )
156 | set(CMAKE_EXE_LINKER_FLAGS_COVERAGE
157 | ""
158 | CACHE STRING "Flags used for linking binaries during coverage builds."
159 | FORCE )
160 | set(CMAKE_SHARED_LINKER_FLAGS_COVERAGE
161 | ""
162 | CACHE STRING "Flags used by the shared libraries linker during coverage builds."
163 | FORCE )
164 | mark_as_advanced(
165 | CMAKE_Fortran_FLAGS_COVERAGE
166 | CMAKE_CXX_FLAGS_COVERAGE
167 | CMAKE_C_FLAGS_COVERAGE
168 | CMAKE_EXE_LINKER_FLAGS_COVERAGE
169 | CMAKE_SHARED_LINKER_FLAGS_COVERAGE )
170 |
171 | if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
172 | message(WARNING "Code coverage results with an optimised (non-Debug) build may be misleading")
173 | endif() # NOT CMAKE_BUILD_TYPE STREQUAL "Debug"
174 |
175 | if(CMAKE_C_COMPILER_ID STREQUAL "GNU" OR CMAKE_Fortran_COMPILER_ID STREQUAL "GNU")
176 | link_libraries(gcov)
177 | endif()
178 |
179 | # Defines a target for running and collection code coverage information
180 | # Builds dependencies, runs the given executable and outputs reports.
181 | # NOTE! The executable should always have a ZERO as exit code otherwise
182 | # the coverage generation will not complete.
183 | #
184 | # setup_target_for_coverage_lcov(
185 | # NAME testrunner_coverage # New target name
186 | # EXECUTABLE testrunner -j ${PROCESSOR_COUNT} # Executable in PROJECT_BINARY_DIR
187 | # DEPENDENCIES testrunner # Dependencies to build first
188 | # BASE_DIRECTORY "../" # Base directory for report
189 | # # (defaults to PROJECT_SOURCE_DIR)
190 | # EXCLUDE "src/dir1/*" "src/dir2/*" # Patterns to exclude (can be relative
191 | # # to BASE_DIRECTORY, with CMake 3.4+)
192 | # NO_DEMANGLE # Don't demangle C++ symbols
193 | # # even if c++filt is found
194 | # )
195 | function(setup_target_for_coverage_lcov)
196 |
197 | set(options NO_DEMANGLE)
198 | set(oneValueArgs BASE_DIRECTORY NAME)
199 | set(multiValueArgs EXCLUDE EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES LCOV_ARGS GENHTML_ARGS)
200 | cmake_parse_arguments(Coverage "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
201 |
202 | if(NOT LCOV_PATH)
203 | message(FATAL_ERROR "lcov not found! Aborting...")
204 | endif() # NOT LCOV_PATH
205 |
206 | if(NOT GENHTML_PATH)
207 | message(FATAL_ERROR "genhtml not found! Aborting...")
208 | endif() # NOT GENHTML_PATH
209 |
210 | # Set base directory (as absolute path), or default to PROJECT_SOURCE_DIR
211 | if(${Coverage_BASE_DIRECTORY})
212 | get_filename_component(BASEDIR ${Coverage_BASE_DIRECTORY} ABSOLUTE)
213 | else()
214 | set(BASEDIR ${PROJECT_SOURCE_DIR})
215 | endif()
216 |
217 | # Collect excludes (CMake 3.4+: Also compute absolute paths)
218 | set(LCOV_EXCLUDES "")
219 | foreach(EXCLUDE ${Coverage_EXCLUDE} ${COVERAGE_EXCLUDES} ${COVERAGE_LCOV_EXCLUDES})
220 | if(CMAKE_VERSION VERSION_GREATER 3.4)
221 | get_filename_component(EXCLUDE ${EXCLUDE} ABSOLUTE BASE_DIR ${BASEDIR})
222 | endif()
223 | list(APPEND LCOV_EXCLUDES "${EXCLUDE}")
224 | endforeach()
225 | list(REMOVE_DUPLICATES LCOV_EXCLUDES)
226 |
227 | # Conditional arguments
228 | if(CPPFILT_PATH AND NOT ${Coverage_NO_DEMANGLE})
229 | set(GENHTML_EXTRA_ARGS "--demangle-cpp")
230 | endif()
231 |
232 | # Setup target
233 | add_custom_target(${Coverage_NAME}
234 |
235 | # Cleanup lcov
236 | COMMAND ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} -directory . -b ${BASEDIR} --zerocounters
237 | # Create baseline to make sure untouched files show up in the report
238 | COMMAND ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} -c -i -d . -b ${BASEDIR} -o ${Coverage_NAME}.base
239 |
240 | # Run tests
241 | COMMAND ${Coverage_EXECUTABLE} ${Coverage_EXECUTABLE_ARGS}
242 |
243 | # Capturing lcov counters and generating report
244 | COMMAND ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} --directory . -b ${BASEDIR} --capture --output-file ${Coverage_NAME}.capture
245 | # add baseline counters
246 | COMMAND ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} -a ${Coverage_NAME}.base -a ${Coverage_NAME}.capture --output-file ${Coverage_NAME}.total
247 | # filter collected data to final coverage report
248 | COMMAND ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} --remove ${Coverage_NAME}.total ${LCOV_EXCLUDES} --output-file ${Coverage_NAME}.info
249 |
250 | # Generate HTML output
251 | COMMAND ${GENHTML_PATH} ${GENHTML_EXTRA_ARGS} ${Coverage_GENHTML_ARGS} -o ${Coverage_NAME} ${Coverage_NAME}.info
252 |
253 | # Set output files as GENERATED (will be removed on 'make clean')
254 | BYPRODUCTS
255 | ${Coverage_NAME}.base
256 | ${Coverage_NAME}.capture
257 | ${Coverage_NAME}.total
258 | ${Coverage_NAME}.info
259 | ${Coverage_NAME} # report directory
260 |
261 | WORKING_DIRECTORY ${PROJECT_BINARY_DIR}
262 | DEPENDS ${Coverage_DEPENDENCIES}
263 | VERBATIM # Protect arguments to commands
264 | COMMENT "Resetting code coverage counters to zero.\nProcessing code coverage counters and generating report."
265 | )
266 |
267 | # Show where to find the lcov info report
268 | add_custom_command(TARGET ${Coverage_NAME} POST_BUILD
269 | COMMAND ;
270 | COMMENT "Lcov code coverage info report saved in ${Coverage_NAME}.info."
271 | )
272 |
273 | # Show info where to find the report
274 | add_custom_command(TARGET ${Coverage_NAME} POST_BUILD
275 | COMMAND ;
276 | COMMENT "Open ./${Coverage_NAME}/index.html in your browser to view the coverage report."
277 | )
278 |
279 | endfunction() # setup_target_for_coverage_lcov
280 |
281 | # Defines a target for running and collection code coverage information
282 | # Builds dependencies, runs the given executable and outputs reports.
283 | # NOTE! The executable should always have a ZERO as exit code otherwise
284 | # the coverage generation will not complete.
285 | #
286 | # setup_target_for_coverage_gcovr_xml(
287 | # NAME ctest_coverage # New target name
288 | # EXECUTABLE ctest -j ${PROCESSOR_COUNT} # Executable in PROJECT_BINARY_DIR
289 | # DEPENDENCIES executable_target # Dependencies to build first
290 | # BASE_DIRECTORY "../" # Base directory for report
291 | # # (defaults to PROJECT_SOURCE_DIR)
292 | # EXCLUDE "src/dir1/*" "src/dir2/*" # Patterns to exclude (can be relative
293 | # # to BASE_DIRECTORY, with CMake 3.4+)
294 | # )
295 | function(setup_target_for_coverage_gcovr_xml)
296 |
297 | set(options NONE)
298 | set(oneValueArgs BASE_DIRECTORY NAME)
299 | set(multiValueArgs EXCLUDE EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES)
300 | cmake_parse_arguments(Coverage "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
301 |
302 | if(NOT GCOVR_PATH)
303 | message(FATAL_ERROR "gcovr not found! Aborting...")
304 | endif() # NOT GCOVR_PATH
305 |
306 | # Set base directory (as absolute path), or default to PROJECT_SOURCE_DIR
307 | if(${Coverage_BASE_DIRECTORY})
308 | get_filename_component(BASEDIR ${Coverage_BASE_DIRECTORY} ABSOLUTE)
309 | else()
310 | set(BASEDIR ${PROJECT_SOURCE_DIR})
311 | endif()
312 |
313 | # Collect excludes (CMake 3.4+: Also compute absolute paths)
314 | set(GCOVR_EXCLUDES "")
315 | foreach(EXCLUDE ${Coverage_EXCLUDE} ${COVERAGE_EXCLUDES} ${COVERAGE_GCOVR_EXCLUDES})
316 | if(CMAKE_VERSION VERSION_GREATER 3.4)
317 | get_filename_component(EXCLUDE ${EXCLUDE} ABSOLUTE BASE_DIR ${BASEDIR})
318 | endif()
319 | list(APPEND GCOVR_EXCLUDES "${EXCLUDE}")
320 | endforeach()
321 | list(REMOVE_DUPLICATES GCOVR_EXCLUDES)
322 |
323 | # Combine excludes to several -e arguments
324 | set(GCOVR_EXCLUDE_ARGS "")
325 | foreach(EXCLUDE ${GCOVR_EXCLUDES})
326 | list(APPEND GCOVR_EXCLUDE_ARGS "-e")
327 | list(APPEND GCOVR_EXCLUDE_ARGS "${EXCLUDE}")
328 | endforeach()
329 |
330 | add_custom_target(${Coverage_NAME}
331 | # Run tests
332 | ${Coverage_EXECUTABLE} ${Coverage_EXECUTABLE_ARGS}
333 |
334 | # Running gcovr
335 | COMMAND ${GCOVR_PATH} --xml
336 | -r ${BASEDIR} ${GCOVR_EXCLUDE_ARGS}
337 | --object-directory=${PROJECT_BINARY_DIR}
338 | -o ${Coverage_NAME}.xml
339 | BYPRODUCTS ${Coverage_NAME}.xml
340 | WORKING_DIRECTORY ${PROJECT_BINARY_DIR}
341 | DEPENDS ${Coverage_DEPENDENCIES}
342 | VERBATIM # Protect arguments to commands
343 | COMMENT "Running gcovr to produce Cobertura code coverage report."
344 | )
345 |
346 | # Show info where to find the report
347 | add_custom_command(TARGET ${Coverage_NAME} POST_BUILD
348 | COMMAND ;
349 | COMMENT "Cobertura code coverage report saved in ${Coverage_NAME}.xml."
350 | )
351 | endfunction() # setup_target_for_coverage_gcovr_xml
352 |
353 | # Defines a target for running and collection code coverage information
354 | # Builds dependencies, runs the given executable and outputs reports.
355 | # NOTE! The executable should always have a ZERO as exit code otherwise
356 | # the coverage generation will not complete.
357 | #
358 | # setup_target_for_coverage_gcovr_html(
359 | # NAME ctest_coverage # New target name
360 | # EXECUTABLE ctest -j ${PROCESSOR_COUNT} # Executable in PROJECT_BINARY_DIR
361 | # DEPENDENCIES executable_target # Dependencies to build first
362 | # BASE_DIRECTORY "../" # Base directory for report
363 | # # (defaults to PROJECT_SOURCE_DIR)
364 | # EXCLUDE "src/dir1/*" "src/dir2/*" # Patterns to exclude (can be relative
365 | # # to BASE_DIRECTORY, with CMake 3.4+)
366 | # )
367 | function(setup_target_for_coverage_gcovr_html)
368 |
369 | set(options NONE)
370 | set(oneValueArgs BASE_DIRECTORY NAME)
371 | set(multiValueArgs EXCLUDE EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES)
372 | cmake_parse_arguments(Coverage "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
373 |
374 | if(NOT GCOVR_PATH)
375 | message(FATAL_ERROR "gcovr not found! Aborting...")
376 | endif() # NOT GCOVR_PATH
377 |
378 | # Set base directory (as absolute path), or default to PROJECT_SOURCE_DIR
379 | if(${Coverage_BASE_DIRECTORY})
380 | get_filename_component(BASEDIR ${Coverage_BASE_DIRECTORY} ABSOLUTE)
381 | else()
382 | set(BASEDIR ${PROJECT_SOURCE_DIR})
383 | endif()
384 |
385 | # Collect excludes (CMake 3.4+: Also compute absolute paths)
386 | set(GCOVR_EXCLUDES "")
387 | foreach(EXCLUDE ${Coverage_EXCLUDE} ${COVERAGE_EXCLUDES} ${COVERAGE_GCOVR_EXCLUDES})
388 | if(CMAKE_VERSION VERSION_GREATER 3.4)
389 | get_filename_component(EXCLUDE ${EXCLUDE} ABSOLUTE BASE_DIR ${BASEDIR})
390 | endif()
391 | list(APPEND GCOVR_EXCLUDES "${EXCLUDE}")
392 | endforeach()
393 | list(REMOVE_DUPLICATES GCOVR_EXCLUDES)
394 |
395 | # Combine excludes to several -e arguments
396 | set(GCOVR_EXCLUDE_ARGS "")
397 | foreach(EXCLUDE ${GCOVR_EXCLUDES})
398 | list(APPEND GCOVR_EXCLUDE_ARGS "-e")
399 | list(APPEND GCOVR_EXCLUDE_ARGS "${EXCLUDE}")
400 | endforeach()
401 |
402 | add_custom_target(${Coverage_NAME}
403 | # Run tests
404 | ${Coverage_EXECUTABLE} ${Coverage_EXECUTABLE_ARGS}
405 |
406 | # Create folder
407 | COMMAND ${CMAKE_COMMAND} -E make_directory ${PROJECT_BINARY_DIR}/${Coverage_NAME}
408 |
409 | # Running gcovr
410 | COMMAND ${GCOVR_PATH} --html --html-details
411 | -r ${BASEDIR} ${GCOVR_EXCLUDE_ARGS}
412 | --object-directory=${PROJECT_BINARY_DIR}
413 | -o ${Coverage_NAME}/index.html
414 |
415 | BYPRODUCTS ${PROJECT_BINARY_DIR}/${Coverage_NAME} # report directory
416 | WORKING_DIRECTORY ${PROJECT_BINARY_DIR}
417 | DEPENDS ${Coverage_DEPENDENCIES}
418 | VERBATIM # Protect arguments to commands
419 | COMMENT "Running gcovr to produce HTML code coverage report."
420 | )
421 |
422 | # Show info where to find the report
423 | add_custom_command(TARGET ${Coverage_NAME} POST_BUILD
424 | COMMAND ;
425 | COMMENT "Open ./${Coverage_NAME}/index.html in your browser to view the coverage report."
426 | )
427 |
428 | endfunction() # setup_target_for_coverage_gcovr_html
429 |
430 | function(append_coverage_compiler_flags)
431 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${COVERAGE_COMPILER_FLAGS}" PARENT_SCOPE)
432 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${COVERAGE_COMPILER_FLAGS}" PARENT_SCOPE)
433 | set(CMAKE_Fortran_FLAGS "${CMAKE_Fortran_FLAGS} ${COVERAGE_COMPILER_FLAGS}" PARENT_SCOPE)
434 | message(STATUS "Appending code coverage compiler flags: ${COVERAGE_COMPILER_FLAGS}")
435 | endfunction() # append_coverage_compiler_flags
436 |
--------------------------------------------------------------------------------
/cmake/googlebenchmark.cmake:
--------------------------------------------------------------------------------
1 | include(ExternalProject)
2 | ExternalProject_Add(googlebenchmark-external
3 | GIT_REPOSITORY https://github.com/google/benchmark.git
4 | GIT_TAG master
5 | PREFIX ${VENDOR_PREFIX}
6 | INSTALL_DIR ${EXTERNAL_INSTALL_DIR}
7 | CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${EXTERNAL_INSTALL_DIR}
8 | -DCMAKE_CXX_STANDARD=${CMAKE_CXX_STANDARD}
9 | -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
10 | -DBENCHMARK_ENABLE_GTEST_TESTS=OFF
11 | )
--------------------------------------------------------------------------------
/cmake/googletest.cmake:
--------------------------------------------------------------------------------
1 | include(ExternalProject)
2 |
3 | ExternalProject_Add(googletest-external
4 | GIT_REPOSITORY https://github.com/google/googletest.git
5 | GIT_TAG master
6 | PREFIX ${VENDOR_PREFIX}
7 | INSTALL_DIR ${EXTERNAL_INSTALL_DIR}
8 | CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${EXTERNAL_INSTALL_DIR}
9 | -DCMAKE_CXX_STANDARD=${CMAKE_CXX_STANDARD}
10 | )
11 |
12 | # Prevent overriding the parent project's compiler/linker settings on Windows
13 | set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
14 | include(GoogleTest)
15 |
--------------------------------------------------------------------------------
/include/hash.hh:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include
5 |
6 | namespace Hash {
7 | /**
8 | * Calculates the 32bit FNV1a hash of a c-string literal.
9 | * @param str String literal to be hashed
10 | * @param n Length of the string.
11 | * @return Calculated hash of the string
12 | */
13 | static constexpr std::uint32_t FNV1a(const char* str, std::size_t n, std::uint32_t hash = UINT32_C(2166136261)) {
14 | return n == 0 ? hash : FNV1a(str + 1, n - 1, (hash ^ str[0]) * UINT32_C(19777619));
15 | }
16 |
17 | /**
18 | * Calculates the 32bit FNV1a hash of a std::string_view literal.
19 | * note: Requires string_view to be a literal in order to be evaluated during compile time!
20 | * @param str String literal to be hashed
21 | * @return Calculated hash of the string
22 | */
23 | static constexpr std::uint32_t FNV1a(std::string_view str) {
24 | return FNV1a(str.data(), str.size());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/include/rtti.hh:
--------------------------------------------------------------------------------
1 | /**
2 | * MIT License
3 | *
4 | * Copyright (c) 2020 Roy van Dam
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 |
25 | #pragma once
26 |
27 | #include
28 | #include
29 | #include
30 | #include
31 |
32 | #include "hash.hh"
33 |
34 | namespace RTTI {
35 | template
36 | constexpr std::string_view TypeName();
37 |
38 | template <>
39 | constexpr std::string_view TypeName()
40 | { return "void"; }
41 |
42 | namespace Detail {
43 | template
44 | constexpr std::string_view WrappedTypeName() {
45 | #ifdef __clang__
46 | return __PRETTY_FUNCTION__;
47 | #elif defined(__GNUC__)
48 | return __PRETTY_FUNCTION__;
49 | #else
50 | #error "Unsupported compiler"
51 | #endif
52 | }
53 |
54 | constexpr std::size_t WrappedTypeNamePrefixLength() {
55 | return WrappedTypeName().find(TypeName());
56 | }
57 |
58 | constexpr std::size_t WrappedTypeNameSuffixLength() {
59 | return WrappedTypeName().length()
60 | - WrappedTypeNamePrefixLength()
61 | - TypeName().length();
62 | }
63 | }
64 |
65 | template
66 | constexpr std::string_view TypeName() {
67 | constexpr auto wrappedTypeName = Detail::WrappedTypeName();
68 | constexpr auto prefixLength = Detail::WrappedTypeNamePrefixLength();
69 | constexpr auto suffixLength = Detail::WrappedTypeNameSuffixLength();
70 | constexpr auto typeNameLength = wrappedTypeName.length() - prefixLength - suffixLength;
71 | return wrappedTypeName.substr(prefixLength, typeNameLength);
72 | }
73 |
74 | /// TypeId type definition
75 | using TypeId = std::uint32_t;
76 |
77 | /// Forward declaration of the Enable base.
78 | struct Enable;
79 |
80 | /**
81 | * Static typeinfo structure for registering types and accessing their information.
82 | */
83 | template
84 | struct TypeInfo {
85 | using T = std::remove_const_t>;
86 |
87 | /// Ensure all passed parents are basses of this type.
88 | static_assert((... && std::is_base_of::value),
89 | "One or more parents are not a base of this type.");
90 |
91 | /// Ensure all passed parent hierarchies have RTTI enabled.
92 | static_assert((... && std::is_base_of::value),
93 | "One or more parent hierarchies is not based on top of RTTI::Enable.");
94 |
95 | /**
96 | * Returns the type string of the type T.
97 | * @returns Type string
98 | */
99 | [[nodiscard]] static constexpr std::string_view Name() noexcept {
100 | return TypeName();
101 | }
102 |
103 | /**
104 | * Returns the type identifier of the type T.
105 | * @returns Type identifier
106 | */
107 | [[nodiscard]] static constexpr TypeId Id() noexcept {
108 | return Hash::FNV1a(Name());
109 | }
110 |
111 | /**
112 | * Checks whether the passed type is the same or a parent of the type.
113 | * @tparam The type to compare the identifier with.
114 | * @returns True in case a match was found.
115 | */
116 | [[nodiscard]] static constexpr bool Is(TypeId typeId) noexcept {
117 | return (Id() == typeId) || (... || (Parents::TypeInfo::Is(typeId)));
118 | }
119 |
120 | /**
121 | * Walks the dependency hierarchy of the object in search of the type identified
122 | * by the passed type id. In case found casts the passed pointer into the passed
123 | * type. If no match can be found the function returns a nullptr.
124 | * @tparam T The type of the most specialized type in the dependency hierarchy.
125 | * @param typeId The identifier of the type to cast the object into.
126 | * @returns Valid pointer to instance of requested type if the object is a
127 | * direct descendance of the type identified by the passed type id. Otherwise
128 | * the value returned is a nullptr.
129 | */
130 | template
131 | [[nodiscard]] static void const* DynamicCast(TypeId typeId, T const* ptr) noexcept {
132 | // Check whether the current type matches the requested type.
133 | if (Id() == typeId) {
134 | // Cast the passed pointer in to the current type and stop
135 | // the recursion.
136 | return static_cast(ptr);
137 | }
138 |
139 | // The current type does not match, recursively invoke the method
140 | // for all directly related parent types.
141 | std::array ptrs = {
142 | Parents::TypeInfo::DynamicCast(typeId, ptr)...};
143 |
144 | // Check whether the traversal up the dependency hierarchy returned a pointer
145 | // that is not null.
146 | auto it = std::find_if(ptrs.begin(), ptrs.end(), [](void const* ptr) {
147 | return ptr != nullptr;
148 | });
149 | return (it != ptrs.end()) ? *it : nullptr;
150 | }
151 | };
152 |
153 | /**
154 | * Parent type for types at the base of an open RTTI hierarchy
155 | */
156 | struct Enable {
157 | virtual ~Enable() = default;
158 |
159 | /**
160 | * Returns the type identifier of the object.
161 | * @returns Type identifier
162 | */
163 | [[nodiscard]] virtual TypeId typeId() const noexcept = 0;
164 |
165 | /**
166 | * Checks whether the object is a direct or derived instance of
167 | * the type identified by the passed identifier.
168 | * @tparam The identifier to compare with.
169 | * @returns True in case a match was found.
170 | */
171 | [[nodiscard]] virtual bool isById(TypeId typeId) const noexcept = 0;
172 |
173 | /**
174 | * Checks whether the object is an instance of child instance of
175 | * the passed type.
176 | * @tparam The type to compare the identifier with.
177 | * @returns True in case a match was found.
178 | */
179 | template
180 | [[nodiscard]] bool is() const noexcept {
181 | return isById(TypeInfo::Id());
182 | }
183 |
184 | /**
185 | * Dynamically cast the object to the passed type. Attempts to find the
186 | * type by its identifier in the dependency hierarchy of the object.
187 | * When found casts the top level `this` pointer to an instance the
188 | * passed type using a static_cast. The pointer offset for types
189 | * dependent on multiple inheritance is hence resolved at compile-time.
190 | * In essence we are always up-casting from the most specialized type.
191 | *
192 | * @tparam T Pointer type to case the object into.
193 | * @returns A valid pointer to an instance of the passed type. Nullptr
194 | * incase the object instance is not a direct descendence of the passed
195 | * type.
196 | */
197 | template
198 | [[nodiscard]] T* cast() noexcept {
199 | return reinterpret_cast(const_cast(_cast(TypeInfo::Id())));
200 | }
201 |
202 | template
203 | [[nodiscard]] T const* cast() const noexcept {
204 | return reinterpret_cast(_cast(TypeInfo::Id()));
205 | }
206 |
207 | protected:
208 | /**
209 | * Used to invoke the _dynamic_cast from the most specialized type in the
210 | * dependency hierarchy by overloaded this function in each derivation of
211 | * RTTI::Extends<>.
212 | * @param typeId The identifier of the type to cast the object into.
213 | * @returns Valid pointer to instance of requested type if the object is a
214 | * direct descendance of the type identified by the passed type id. Otherwise
215 | * the value returned is a nullptr.
216 | */
217 | [[nodiscard]] virtual void const* _cast(TypeId typeId) const noexcept = 0;
218 | };
219 | } // namespace RTTI
220 |
221 | /**
222 | * Macro to be called in the body of each type declaration that is to be part of an
223 | * open hierarchy RTTI structure. The type itself or one or more parents of the type
224 | * need to have been derived from RTTI::Enable.
225 | * @param T The type it self.
226 | * @param Parents Variadic number of direct parrent types of the type
227 | */
228 | #define RTTI_DECLARE_TYPEINFO(T, ...) \
229 | public: \
230 | using TypeInfo = RTTI::TypeInfo; \
231 | [[nodiscard]] virtual RTTI::TypeId typeId() const noexcept override { \
232 | return TypeInfo::Id(); \
233 | } \
234 | [[nodiscard]] virtual bool isById(RTTI::TypeId typeId) const noexcept override { \
235 | return TypeInfo::Is(typeId); \
236 | } \
237 | \
238 | protected: \
239 | [[nodiscard]] virtual void const* _cast(RTTI::TypeId typeId) const noexcept override { \
240 | return TypeInfo::Is(typeId) ? TypeInfo::DynamicCast(typeId, this) : nullptr; \
241 | }
242 |
--------------------------------------------------------------------------------
/tests/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | file(GLOB SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/*_test.cc)
2 |
3 | add_executable(rtti-tests ${SOURCES})
4 | target_link_libraries(rtti-tests PRIVATE rtti gmock gtest gtest_main pthread)
5 | target_include_directories(rtti-tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
6 | clang_format(rtti-tests ${SOURCES})
7 |
8 | add_dependencies(rtti-tests googletest-external)
9 | gtest_discover_tests(rtti-tests)
10 |
11 | if(ENABLE_CODE_COVERAGE)
12 | setup_target_for_coverage_gcovr_html(
13 | NAME coverage
14 | EXECUTABLE ctest -j ${PROCESSOR_COUNT}
15 | DEPENDENCIES rtti-tests
16 | EXCLUDE "build/*"
17 | )
18 | endif()
--------------------------------------------------------------------------------
/tests/hierarchy_test.cc:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | #include
4 |
5 | struct GrandParent : virtual RTTI::Enable {
6 | RTTI_DECLARE_TYPEINFO(GrandParent);
7 | };
8 |
9 | struct ParentA : virtual GrandParent {
10 | RTTI_DECLARE_TYPEINFO(ParentA, GrandParent);
11 | };
12 |
13 | struct ParentB : virtual GrandParent {
14 | RTTI_DECLARE_TYPEINFO(ParentB, GrandParent);
15 | };
16 |
17 | struct ChildA
18 | : ParentA
19 | , ParentB {
20 | RTTI_DECLARE_TYPEINFO(ChildA, ParentA, ParentB);
21 | };
22 |
23 | struct ChildB
24 | : ParentA
25 | , ParentB {
26 | RTTI_DECLARE_TYPEINFO(ChildB, ParentA, ParentB);
27 | };
28 |
29 | template
30 | struct ChildT
31 | : ParentA
32 | , ParentB {
33 | RTTI_DECLARE_TYPEINFO(ChildT, ParentA, ParentB);
34 | };
35 |
36 | namespace {
37 | TEST(HierarchyTest, TypeIdentification) {
38 | ChildA childA;
39 | EXPECT_EQ(childA.typeId(), RTTI::TypeInfo::Id());
40 | EXPECT_TRUE(childA.is());
41 | EXPECT_FALSE(childA.is());
42 | EXPECT_TRUE(childA.is());
43 | EXPECT_TRUE(childA.is());
44 | EXPECT_TRUE(childA.is());
45 |
46 | ChildB childB;
47 | EXPECT_EQ(childB.typeId(), RTTI::TypeInfo::Id());
48 | EXPECT_FALSE(childB.is());
49 | EXPECT_TRUE(childB.is());
50 | EXPECT_TRUE(childB.is());
51 | EXPECT_TRUE(childB.is());
52 | EXPECT_TRUE(childB.is());
53 |
54 | ChildT childTi;
55 | EXPECT_EQ(childTi.typeId(), RTTI::TypeInfo>::Id());
56 | EXPECT_FALSE(childTi.is>());
57 | EXPECT_TRUE(childTi.is>());
58 | EXPECT_TRUE(childTi.is());
59 | EXPECT_TRUE(childTi.is());
60 | EXPECT_TRUE(childTi.is());
61 |
62 | ChildT childTf;
63 | EXPECT_EQ(childTf.typeId(), RTTI::TypeInfo>::Id());
64 | EXPECT_FALSE(childTf.is>());
65 | EXPECT_TRUE(childTf.is>());
66 | EXPECT_TRUE(childTf.is());
67 | EXPECT_TRUE(childTf.is());
68 | EXPECT_TRUE(childTf.is());
69 |
70 | EXPECT_NE(childTi.typeId(), childTf.typeId());
71 |
72 | ParentA parentA;
73 | EXPECT_EQ(parentA.typeId(), RTTI::TypeInfo::Id());
74 | EXPECT_FALSE(parentA.is());
75 | EXPECT_FALSE(parentA.is());
76 | EXPECT_TRUE(parentA.is());
77 | EXPECT_FALSE(parentA.is());
78 | EXPECT_TRUE(parentA.is());
79 |
80 | ParentB parentB;
81 | EXPECT_EQ(parentB.typeId(), RTTI::TypeInfo::Id());
82 | EXPECT_FALSE(parentB.is());
83 | EXPECT_FALSE(parentB.is());
84 | EXPECT_FALSE(parentB.is());
85 | EXPECT_TRUE(parentB.is());
86 | EXPECT_TRUE(parentB.is());
87 |
88 | GrandParent grandParent;
89 | EXPECT_EQ(grandParent.typeId(), RTTI::TypeInfo::Id());
90 | EXPECT_FALSE(grandParent.is());
91 | EXPECT_FALSE(grandParent.is());
92 | EXPECT_FALSE(grandParent.is());
93 | EXPECT_FALSE(grandParent.is());
94 | EXPECT_TRUE(grandParent.is());
95 | }
96 |
97 | TEST(HierarchyTest, UpCasting) {
98 | ChildA childA;
99 | EXPECT_EQ(childA.cast(), &childA);
100 | EXPECT_EQ(childA.cast(), nullptr);
101 | EXPECT_EQ(childA.cast(), dynamic_cast(&childA));
102 | EXPECT_EQ(childA.cast(), static_cast(&childA));
103 | EXPECT_EQ(childA.cast(), dynamic_cast(&childA));
104 | EXPECT_EQ(childA.cast(), static_cast(&childA));
105 | EXPECT_EQ(childA.cast(), dynamic_cast(&childA));
106 | EXPECT_EQ(childA.cast(), static_cast(&childA));
107 | }
108 |
109 | TEST(HierarchyTest, UpCastingConst) {
110 | ChildA const childA;
111 | EXPECT_EQ(childA.cast(), &childA);
112 | EXPECT_EQ(childA.cast(), nullptr);
113 | EXPECT_EQ(childA.cast(), dynamic_cast(&childA));
114 | EXPECT_EQ(childA.cast(), static_cast(&childA));
115 | EXPECT_EQ(childA.cast(), dynamic_cast(&childA));
116 | EXPECT_EQ(childA.cast(), static_cast(&childA));
117 | EXPECT_EQ(childA.cast(), dynamic_cast(&childA));
118 | EXPECT_EQ(childA.cast(), static_cast(&childA));
119 | }
120 |
121 | TEST(HierarchyTest, DownCasting) {
122 | ChildA childA;
123 | GrandParent* grandParent = childA.cast();
124 |
125 | EXPECT_EQ(grandParent->cast(), &childA);
126 | EXPECT_EQ(grandParent->cast(), nullptr);
127 | EXPECT_EQ(grandParent->cast(), dynamic_cast(&childA));
128 | EXPECT_EQ(grandParent->cast(), static_cast(&childA));
129 | EXPECT_EQ(grandParent->cast(), dynamic_cast(&childA));
130 | EXPECT_EQ(grandParent->cast(), static_cast(&childA));
131 | EXPECT_EQ(grandParent->cast(), dynamic_cast(&childA));
132 | EXPECT_EQ(grandParent->cast(), static_cast(&childA));
133 | }
134 |
135 | TEST(HierarchyTest, DownCastingConst) {
136 | ChildA const childA;
137 | GrandParent const* grandParent = childA.cast();
138 |
139 | EXPECT_EQ(grandParent->cast(), &childA);
140 | EXPECT_EQ(grandParent->cast(), nullptr);
141 | EXPECT_EQ(grandParent->cast(), dynamic_cast(&childA));
142 | EXPECT_EQ(grandParent->cast(), static_cast(&childA));
143 | EXPECT_EQ(grandParent->cast(), dynamic_cast(&childA));
144 | EXPECT_EQ(grandParent->cast(), static_cast(&childA));
145 | EXPECT_EQ(grandParent->cast(), dynamic_cast(&childA));
146 | EXPECT_EQ(grandParent->cast(), static_cast(&childA));
147 | }
148 |
149 | } // namespace
150 |
--------------------------------------------------------------------------------
/tests/typeinfo_test.cc:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | #include
4 | #include
5 |
6 | using namespace RTTI;
7 |
8 | TEST(TypeInfo, SameTypesAreEqual) {
9 | EXPECT_EQ(typeid(int), typeid(int));
10 | EXPECT_EQ(TypeInfo::Id(), TypeInfo::Id());
11 | }
12 |
13 | TEST(TypeInfo, DifferentTypesAreNotEqual) {
14 | EXPECT_NE(typeid(int), typeid(bool));
15 | EXPECT_NE(TypeInfo::Id(), TypeInfo::Id());
16 | }
17 |
18 | TEST(TypeInfo, TypesAndPointerToTypesAreNotEqual) {
19 | EXPECT_NE(typeid(int), typeid(int*));
20 | EXPECT_NE(TypeInfo::Id(), TypeInfo::Id());
21 | }
22 |
23 | TEST(TypeInfo, PointerAndConstPointerTypesAreNotEqual) {
24 | EXPECT_NE(typeid(int*), typeid(int const*));
25 | EXPECT_NE(TypeInfo::Id(), TypeInfo::Id());
26 | }
27 |
28 | TEST(TypeInfo, NonConstAndConstTypesAreEqual) {
29 | EXPECT_EQ(typeid(int), typeid(int const));
30 | EXPECT_EQ(TypeInfo::Id(), TypeInfo::Id());
31 |
32 | EXPECT_EQ(typeid(int*), typeid(int* const));
33 | EXPECT_EQ(TypeInfo::Id(), TypeInfo::Id());
34 | }
35 |
36 | TEST(TypeInfo, TypesAndReferencesToTypesAreEqual) {
37 | EXPECT_EQ(typeid(int), typeid(int&));
38 | EXPECT_EQ(TypeInfo::Id(), TypeInfo::Id());
39 |
40 | EXPECT_EQ(typeid(int), typeid(int&&));
41 | EXPECT_EQ(TypeInfo::Id(), TypeInfo::Id());
42 | }
--------------------------------------------------------------------------------