【问题标题】:Create compiler error when constructor input contains duplicates当构造函数输入包含重复项时创建编译器错误
【发布时间】:2021-10-01 13:01:41
【问题描述】:

考虑以下类:

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"}){}

};

我想通过创建编译时错误来禁止来自基类的派生类,其中初始化列表包含重复项。例如,不应允许以下类:

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

};

我最初的方法是尝试模板化 Base 类。但是,据我确定,我不能使用非类型模板参数,即使在可以将字符串作为参数传递的 C++20 中(我相信在 C++20 方法中您只能传递一个字符串)。

我的下一个方法是编写一个 constexpr 函数来确定单词是否唯一

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

然后重写Base类如下:

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_;
};

虽然这个函数在类外部工作,但在 Base 构造函数内部,编译器告诉我不能将构造函数参数的值用作常量。当然,我可以编写一个运行时检查,但我真的想阻止在编译时在初始化列表中创建重复的单词。这可能吗?

【问题讨论】:

  • stackoverflow.com/questions/68586930/… stackoverflow.com/questions/59610746/… the compiler tells me that I cannot use the value of the constructor parameter as a constant 要么采用 constexpr 方式。做:constexpr Derived_BAD var;Derived_BAD() 构造函数也必须是 constexpr
  • std::initializer_list 并不意味着存储为类的成员。它是用来初始化一个实际的容器(例如std::vector 可以用std::initializer_list 初始化)。此外,您真的需要生成编译时错误吗?因为我的意思是,如果您改用std::set,则可以保证设计上没有重复。如果您提供重复,它将被忽略。
  • 为了模仿标准,您可以在类的文档中警告在子类中使用重复项会调用未定义的行为。从那里您没有理由对其进行测试,因为您可以放心地假设它不会发生。
  • @Fareanor 关于集合的好点子,因为它确实解决了未定义行为的问题。

标签: c++ compilation metaprogramming


【解决方案1】:

我最初的方法是尝试模板化 Base 类。但是,据我确定,我不能使用非类型模板参数,即使在可以将字符串作为参数传递的 C++20 中(我相信在 C++20 方法中您只能传递一个字符串)。

当然,在 C++20 中,您可以将多个字符串作为非类型模板参数。

本质上,您可以做的是有一个名为fixed_string 的小包装类,它可以隐式地与const char* 相互转换。而且从 C++20 开始,可以将琐碎的结构体作为非类型模板参数,因此可以将这个轻量级包装类用作非类型模板参数。

这是一个 C++20 实现,可用于实现您想要做的事情:

#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 {};

现在你终于可以这样做了:

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"> {};

这里有一个链接,您可以自己尝试一下:

Demo

【讨论】:

    【解决方案2】:

    要检查任何内容,您必须使用 constexpr 构造类。要在 constexpr 函数中触发编译时错误,您可以抛出,参见 Generate compile-time error if compile-time-constant parameter is wrong

    #include <initializer_list>
    #include <type_traits>
    #include <array>
    #include <stdexcept>
    
    template<std::size_t N>
    constexpr bool unique_words(const std::array<const char*, N>& words) {
        // TODO: implement real logic here
        return words[0][0] == 'd';
    }
    
    template<std::size_t N>
    struct Base {
        constexpr Base(const std::array<const char*, N>& words) 
            : words_(words)
        {
            if (!unique_words<N>(words)) {
                 throw std::invalid_argument("Base must take unique words!");
            }
        }
        std::array<const char*, N> words_;
    };
    
    
    struct Derived_BAD : public Base<4> {
        constexpr Derived_BAD() : Base{{"e", "car", "time", "car"}} {}
    };
    
    int main() {
        constexpr Derived_BAD var; // compile-time error - can't throw in constexpr
        Derived_BAD var2; // will throw at runtime
    }
    

    不要在课堂上存储std::initializer_list!!见using std::initializer_list as a member variable

    构造constexpr时,编译时检查是不可能的。如果您真的想要检查这种情况,可以使用 GNU __builtin_constant_p 扩展并启用编译器优化,请参阅 GNU 文档和 Enable static checks for constant evaluationHow to get a compile time error in constant evaluated expression?

    【讨论】:

    • 优秀的答案,关于std::initializer_list 的观点与@Fareanor 的评论非常吻合。
    • 可以使用consteval代替constexpr,那么这两个变量都会导致编译时错误。
    猜你喜欢
    • 2017-07-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-12-23
    • 2015-06-10
    • 1970-01-01
    相关资源
    最近更新 更多