from SFINAE to concept (2: Hello Concept)

发布于 2023-04-10  209 次阅读


Introduction

C++ Concept是C++20的四个重大更新的一部分(其他三个分别是Module, Coroutine 和 Format),取代了经典的SFINAE,使得模板的约束更加明朗且可以复用。

取代enable_if_t

原代码如下

enum class NumType
{
    NONE,
    INTEGER,
    FLOAT
};

template<class, class = void>
struct get_type
{
    static constexpr NumType type = NumType::NONE;
};

template<class T>
struct get_type<T ,enable_if_t<is_integral_v<T>>>
{
    static constexpr NumType type = NumType::INTEGER;
};

template<class T>
struct get_type<T, enable_if_t<is_floating_point_v<T>>>
{
    static constexpr NumType type = NumType::FLOAT;
};

template<typename Ty>
constexpr auto get_type_v = get_type<Ty>::type;

可以被替换为

template<class>
struct get_type_c
{
    static constexpr auto type = NumType::NONE;
};

template<class Ty> requires is_floating_point_v<Ty>
struct get_type_c<Ty>
{
    static constexpr auto type = NumType::FLOAT;
};

template<class Ty> requires is_integral_v<Ty>
struct get_type_c<Ty>
{
    static constexpr auto type = NumType::INTEGER;
};

template<class Ty>
constexpr auto get_type_c_v = get_type_c<Ty>::type;

取代void_t

原代码如下

#define has_sth_sfinae(func)                                                    \
                                                                                \
    template<class Ty, class = void>                                            \
    struct has_##func  : false_type                                             \
    {                                                                           \
                                                                                \
    };                                                                          \
                                                                                \
    template<class Ty>                                                          \
    struct has_##func<Ty, void_t<decltype(declval<Ty>().func())>> : true_type   \
    {                                                                           \
                                                                                \
    };                                                                          \
                                                                                \
    template< class Ty >                                                        \
    inline constexpr auto has_##func##_v = has_##func##<Ty>::value;             \
                                                                                \
    template< class Ty >                                                        \
    inline constexpr auto has_not_##func##_v = !has_##func##<Ty>::value   


has_sth_sfinae(to_string);
has_sth_sfinae(print);

template <class help_dbg, class = enable_if_t<has_not_to_string_v< help_dbg > && has_print_v<help_dbg>>>
void debug_print(help_dbg&& object)
{
    cout << object.to_string() << '\n';
}

template <class help_dbg, class = enable_if_t<has_to_string_v< help_dbg>&& has_not_print_v<help_dbg>>>
void debug_print(help_dbg&& object)
{
    object.print();
}

可以被替换为

template<typename T>
concept HasToString = requires (T && t) { t.to_string(); };

template<typename T>
concept HasPrint = requires (T && t) { t.print(); };

template<typename T>
concept HasNotBoth = 1 >= (HasPrint<T> + HasToString<T>);

template<typename T> requires HasNotBoth<T>
void print(T&& t)
{
    if constexpr (HasToString<T>)
        cout << t.to_string() << '\n';
    else if constexpr (HasPrint<T>)
        t.print();
    else
        cout << "have nothing\n";
}

更多语法

匿名requires

对于之前的print和to_string两个函数同时出现的时候还没法处理,在这里处理以下

template<class T>
void print(T &&) requires (requires (T&& t) { t.print(); }) && (requires (T&& t) { t.to_string(); })
{
    cout << "have both!!\n";
}

当你不想声明一个concept的时候就可以直接使用requires来定义一个匿名concept,因为concept本身就等于一个requires表达式。

typename<class...>
concept xx = requires ...

上面那样写依然很臃肿,所以我们把它们写在一起

template<class T>
void print(T&&) requires requires (T&& t) { t.print(); t.to_string(); }
{
    cout << "have both!!\n";
}

也许你注意到了,之前的requires在模板声明后面,现在为什么在函数后面?
这是因为C++为了迎合不同习惯,让编译器认为这两种写法是等效的。

套娃

还记得我们已经定义了HasToString了吗?我们可以使用它们,我们改造之前的代码如下

template<class T>
void print(T&&) requires requires { requires HasPrint<T>; requires HasToString<T>; }
{
    cout << "have both!!\n";
}

后面的scope可以接受参数,比如我们可以这样写

template<class T>
void print(T&&) requires requires(T&& t) { requires HasPrint<decltype(t)>; requires HasToString<decltype(t)>; }
{
    cout << "have both!!\n";
}

表达式转换

concept还有一个更美好的特性,就是requires表达式内可以塞一个箭头,这代表着后面模板结构体/类的第一个参数的填充,比如我限定print的返回值必须是void(这里还有一层语义,就是t必须存在print方法)

{ t.print() } -> same_as<void>;

如果我们用type_traits就是这样

same_as < invoke_result_t< decltype (decltype(X{})::print), X >, void >

很好,那么我们把之前的have both函数约束以下

template<class T> requires requires(T&& t) 
{ { t.print() } -> same_as<void>; {t.to_string()} -> convertible_to<const char*>;}
void print(T&&)
{
    cout << "have both!!\n";
}

练习

现在看下面的复杂concept应该是容易且直观的

template <class T>
concept Semiregular = DefaultConstructible<T> &&
    CopyConstructible<T> && Destructible<T> && CopyAssignable<T> &&
requires(T a, size_t n) {  
    requires Same<T*, decltype(&a)>;  // nested: "Same<...> evaluates to true"
    { a.~T() } noexcept;  // compound: "a.~T()" is a valid expression that doesn't throw
    requires Same<T*, decltype(new T)>; // nested: "Same<...> evaluates to true"
    requires Same<T*, decltype(new T[n])>; // nested
    { delete new T };  // compound
    { delete new T[n] }; // compound
};