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?
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: