├── .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 | } --------------------------------------------------------------------------------