Search code examples
c++templatesinheritancec++20template-meta-programming

Prevent multiple derivations of template class with the same parameter value?


I have an inheritance hierarchy that uses CRTP for static polymorphism. One of the template parameters is an integer value, and I'd like to ensure that this number is unique across all derivations, such that the following isn't allowed:

template<class R, quint16 N>
class Base
{...};

class DerA : Base<DerA, 10>
{...};

class DerB : Base<DerB, 10>
{...}; // Error - 10 is already in-use

Erroring via a static_assert() would of course be preferable so that the error message is clear, but any prevention of compilation is fine.

I figure this might be possible via some kind of type traits class, but it's tricky given that the base class isn't actually shared by the derived classes, since the base will be a different class depending on template parameters, and if I used a shared non-template base then it can no longer interact with the template parameters at all. This is definitely veering into the "arcane" realm for me given my lack of template metaprogramming experience.

If this can be done, but only via additional code other than creating the derived class itself (i.e. via a macro), that's also fine, but only if compilation can also be prevented if said code is missing. So essentially, nothing that could easily be forgotten with no immediate signs that something is wrong. This also cannot rely on all values for N being known ahead of time as the exact derivations are not fixed/are extensible in user code.

The goal, if it matters, is for the Derived classes to represent different State/Error types that each have a guaranteed unique "code" associated with them that's accessible via a static member and virtual method. I believe I can achieve this check at the start of runtime by having an extra non-template base with a SIOF protected std::set that the derived classes automatically register into via a static member in template base that calls insert in its constructor (with a guarding contains check), but somehow handling this at compile time would of course be preferred.


Solution

  • There's one thing preventing you from doing this at compile time: the possibility of defining the derived classes in different translation units.

    This leaves the possibility of using the linker for this kind of check. However with the possibility of anonymus namespaces being involved and the possibility of getting libraries of different types involved, it would be hard to ensure such a constraint.

    My recommendation would be to use "code info generation tool" for this purpose. In fact since the compiler is required to output the second parameter of static_assert as part of the error message, you could be using this functionality to extract the required info from the code when compiling with a specific preprocessor symbol specified. This of course requires you to know about all translation units possibly containing definitions of the subclasses.

    If the following code is used in all places to generate the subclasses for this purpose:

    base.hpp

    #ifndef BASE_HPP
    #define BASE_HPP
    
    #include <algorithm>
    #include <limits>
    #include <string_view>
    #include <type_traits>
    
    namespace my_types
    {
    using IdType = int;
    
    namespace impl
    {
    constexpr bool IsValidId(std::string_view sv)
    {
        using namespace std::string_view_literals;
    
        static_assert((std::numeric_limits<IdType>::max)() == 2147483647);
        constexpr auto MaxInt = "2147483647"sv;
    
        if (sv.empty())
        {
            return false;
        }
        if (sv[0] == '0')
        {
            // exclude oct literals
            return false;
        }
    
        for (auto& c : sv)
        {
            if ((c < '0') && (c > '9'))
            {
                return false;
            }
        }
    
        return ((sv.size() < MaxInt.size()) || ((sv.size() == MaxInt.size()) && (sv < MaxInt)));
    }
    } // namespace impl
    
    template<class R, IdType N>
    class Base
    {
    };
    
    } // namespace my_types
    
    #ifdef EXTRACT_IDS
    #   define EXTRACT_ID_SUBSTR "TYPE_EXTRATION_ID"
    #   define EXTRACT_ID(x,n) static_assert(false, EXTRACT_ID_SUBSTR "_" #x "|" #n "_" EXTRACT_ID_SUBSTR);
    #else
    #   define EXTRACT_ID(x,n)
    #endif
    
    #define DEFINE_DERIVED_TYPE(Name, n)                                                    \
    static_assert(::my_types::impl::IsValidId(#n), "'" #n "' is not a integral literal");   \
    EXTRACT_ID(Name,n)                                                                      \
    class Name : ::my_types::Base<Name, n>
    
    #endif // BASE_HPP
    

    a.cpp

    #include "base.hpp"
    
    namespace
    {
    DEFINE_DERIVED_TYPE(DerA, 10)
    {
    };
    }
    

    b.cpp

    #include "base.hpp"
    
    namespace
    {
    DEFINE_DERIVED_TYPE(DerB, 10)
    {
    };
    }
    

    makefile

    CXX=/usr/bin/g++
    CXX_FLAGS= -std=c++17
    
    SOURCES=a.cpp b.cpp
    
    code_test.cpp: $(SOURCES)
        (($(CXX) -DEXTRACT_IDS $(CXX_FLAGS) -o dummy $(SOURCES) 2>&1 || exit 0) | \
            sed -n 's/.*TYPE_EXTRATION_ID_\(.*\)|\([0-9][0-9]*\)_TYPE_EXTRATION_ID.*/#ifdef _CODE_\2\n _CODE_\2(\1)\n#else\n#define _CODE_\2(x) static_assert(false, "duplicate code \2 for class " #x ": was previously defined for class \1");\n#endif/p' ; \
            echo "") > code_test.cpp
    
    
    main: $(SOURCES) codes.hpp code_test.cpp
        $(CXX) $(CXX_FLAGS) -o main $(SOURCES) code_test.cpp
    
    all: main
    

    You may however be better of collecting a global collection of codes for deriveds classes at runtime, probably restricted to debug configuration, to trigger asserts for duplicated values...