Search code examples
c++compilationmetaprogramming

Create compiler error when constructor input contains duplicates


Consider the following classes:

class Base {
public:
    Base(const std::initializer_list<const char*>& words) 
        : words_(words) {}
    std::initializer_list<const char*> words_;
};

class Derived_OK : public Base
{
public:
    Derived_OK()
        : Base({ "dog", "car", "time"}){}

};

I would like to disallow derived classes from the Base class where the initializer list contains duplicates by creating a compile time error. For example the following class should not be allowed:

class Derived_BAD : public Base
{
public:
    Derived_BAD()
        : Base({ "dog", "car", "time", "car"}){} // do not want to allow duplicates at compile time

};

My initial approach was to try templating the Base class. However, as far as I have determined I cannot use non-type template parameters, even in C++20 where a string can be passed as a parameter (I believe you can pass only one string in the C++20 approach).

My next approach was to write a constexpr function to determine if the words are unique

constexpr bool unique_words(const std::initializer_list<const char*>& words);

and then rewrite the Base class as follows:

class Base {
public:
    constexpr Base(const std::initializer_list<const char*>& words) 
        : words_(words)
    {
        static_assert(unique_words(words));
    }
    std::initializer_list<const char*> words_;
};

Although this function works outside of the class, inside of the Base constructor the compiler tells me that I cannot use the value of the constructor parameter as a constant. Of course, I could write a run time check, but I really want to discourage creating duplicate words in the initializer list at compile time. Is this possible?


Solution

  • My initial approach was to try templating the Base class. However, as far as I have determined I cannot use non-type template parameters, even in C++20 where a string can be passed as a parameter (I believe you can pass only one string in the C++20 approach).

    Well, certainly you can have more than one string as a non-type template parameter in C++20.

    Essentially, what you could do is have a little wrapper class called fixed_string which is implicitly convertible to and from a const char*. And since C++20, one can have trivial structs as non-type template parameters because of which one can use this lightweight wrapper class as a non-type template parameter.

    Here is a C++20 implementation which can be used to achieve what you want to do:

    #include <cstddef>
    #include <type_traits>
    
    // The wrapper class in question
    template <std::size_t N>
    struct fixed_string {
        char str[N + 1] {};
        constexpr fixed_string(const char* X) {
            for (std::size_t i = 0; i < N; i++)
                str[i] = X[i];
        }
        constexpr operator const char*() const {
            return str;
        }
    };
    template <std::size_t N>
    fixed_string(const char (&)[N]) -> fixed_string<N - 1>;
    
    // Stores a list of 'fixed_string's
    template <fixed_string ...Strings>
    struct fixed_string_list;
    
    // Concatenates two 'fixed_string_list's together
    template <typename, typename>
    struct fixed_string_list_concat;
    
    template <fixed_string ...Strings1, fixed_string ...Strings2>
    struct fixed_string_list_concat<fixed_string_list<Strings1...>, fixed_string_list<Strings2...>> {
        using type = fixed_string_list<Strings1..., Strings2...>;
    };
    
    // Fetches the string at the specified index in the 'fixed_string_list', the required string is put inside a 'fixed_string_list' and then returned back
    template <typename, std::size_t>
    struct fixed_string_list_get;
    
    template <std::size_t I, fixed_string String1, fixed_string ...Strings>
    struct fixed_string_list_get<fixed_string_list<String1, Strings...>, I> {
        using type = typename fixed_string_list_get<fixed_string_list<Strings...>, I - 1>::type;
    };
    
    template <fixed_string String1, fixed_string ...Strings>
    struct fixed_string_list_get<fixed_string_list<String1, Strings...>, 0> {
        using type = fixed_string_list<String1>;
    };
    
    // Trims the list in the range [From, To)
    template <typename, std::size_t, std::size_t>
    struct fixed_string_list_trim;
    
    template <std::size_t From, std::size_t To, fixed_string ...Strings>
    requires (From < To)
    struct fixed_string_list_trim<fixed_string_list<Strings...>, From, To> {
        using type = typename fixed_string_list_concat<typename fixed_string_list_get<fixed_string_list<Strings...>, From>::type, typename fixed_string_list_trim<fixed_string_list<Strings...>, From + 1, To>::type>::type;
    };
    
    template <std::size_t From, std::size_t To, fixed_string ...Strings>
    requires (From >= To)
    struct fixed_string_list_trim<fixed_string_list<Strings...>, From, To> {
        using type = fixed_string_list<>;
    };
    
    // Returns the 'fixed_string_list' excluding the string at the specified index
    template <typename, std::size_t>
    struct fixed_string_list_exclude;
    
    template <std::size_t I, fixed_string ...Strings>
    requires (I > 0 && I < sizeof...(Strings) - 1)
    struct fixed_string_list_exclude<fixed_string_list<Strings...>, I> {
        using type = typename fixed_string_list_concat<typename fixed_string_list_trim<fixed_string_list<Strings...>, 0, I>::type, typename fixed_string_list_trim<fixed_string_list<Strings...>, I + 1, sizeof...(Strings) - I + 1>::type>::type;
    };
    
    template <std::size_t I, fixed_string ...Strings>
    requires (I == 0)
    struct fixed_string_list_exclude<fixed_string_list<Strings...>, I> {
        using type = typename fixed_string_list_trim<fixed_string_list<Strings...>, 1, sizeof...(Strings)>::type;
    };
    
    template <std::size_t I, fixed_string ...Strings>
    requires (I == sizeof...(Strings) - 1)
    struct fixed_string_list_exclude<fixed_string_list<Strings...>, I> {
        using type = typename fixed_string_list_trim<fixed_string_list<Strings...>, 0, I>::type;
    };
    
    // Checks whether a 'fixed_string_list' contains a given string, the string to be found must also be within a 'fixed_string_list'
    template <typename, typename>
    struct fixed_string_list_contains;
    
    template <fixed_string String, fixed_string ...Strings>
    struct fixed_string_list_contains<fixed_string_list<Strings...>, fixed_string_list<String>> : std::bool_constant<((String == Strings) || ...)> {};
    
    // Implementation detail for 'is_fixed_string_list_unique'
    template <typename, std::size_t, std::size_t>
    struct is_fixed_string_list_unique_impl;
    
    template <std::size_t I, std::size_t Limit, fixed_string ...Strings>
    struct is_fixed_string_list_unique_impl<fixed_string_list<Strings...>, I, Limit> : std::bool_constant<!fixed_string_list_contains<typename fixed_string_list_exclude<fixed_string_list<Strings...>, I>::type, typename fixed_string_list_get<fixed_string_list<Strings...>, I>::type>::value && is_fixed_string_list_unique_impl<fixed_string_list<Strings...>, I + 1, Limit>::value> {};
    
    template <std::size_t I, fixed_string ...Strings>
    struct is_fixed_string_list_unique_impl<fixed_string_list<Strings...>, I, I> : std::true_type {};
    
    // Checks whether the given 'fixed_string_list' has no repeating strings inside
    template <typename>
    struct is_fixed_string_list_unique;
    
    template <fixed_string ...Strings>
    requires (sizeof...(Strings) > 1)
    struct is_fixed_string_list_unique<fixed_string_list<Strings...>> : std::bool_constant<is_fixed_string_list_unique_impl<fixed_string_list<Strings...>, 0, sizeof...(Strings)>::value> {};
    
    template <fixed_string ...Strings>
    requires (sizeof...(Strings) <= 1)
    struct is_fixed_string_list_unique<fixed_string_list<Strings...>> : std::true_type {};
    

    Now you can finally do something like this:

    template <fixed_string ...Strings>
    struct Base {
        static_assert(is_fixed_string_list_unique<fixed_string_list<Strings...>>(), "Duplicate strings are not allowed!");
    };
    
    struct Derived_OK : public Base<"dog", "car", "time"> {};
    
    // Results in a static assertion failure: "Duplicate strings are not allowed!"
    struct Derived_BAD : public Base<"dog", "car", "time", "car"> {};
    

    Here's a link where you can try it out for yourself:

    Demo