from SFINAE to concept (1: introduce to SFINAE)

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


写在前面

随着C++20各个特性的逐一实现(Module除外),C++17也逐渐成为历史。在这里我希望你应该尽量学习并使用一些新的东西而不是用古董标准重复的造轮子。
但是模板元编程毕竟门槛较高,大量历史代码读起来非常困难,所以学习一些模板的技巧是必要的。

what

SFINAE是 Substitution failure is not an error的简写,所以不用纠结它的发音。它和模板有关。我们知道在C++中模板函数/类并不是实体,只有当它们被实例化的时候才会产生对应的代码。而模板函数/类可以被特化、偏特化,找到一个对应的模板的过程被称为匹配,而匹配失败并不是一个错误(SFINAE)。

why

我们如果想实现这样一段代码,如果类型T是字符串,那么函数应该变成一种模样,如果T是整数,函数应该变成另一种模样,如下所示:

template<class Ty>
void print(Ty val)
{
    printf("default type");
}

template<>
void print<const char*>(const char* msg)
{
    printf(msg);
}

template<>
void print<int>(int val)
{
    printf("integer");
}

我们知道模板匹配应当从特化版本开始,如果匹配失败是一种错误,那么如果传进来一个其他类型(比如浮点数),那么直接会产生编译错误(Error)。这是灾难性的,为了防止这种情况发生,就制定了这个规则。

下面举两个标准库用来搞匹配的小工具。

C++11: enable_if / C++17: enable_if_t

what

先给出定义

template<bool, class T = void>
struct enable_if
{
    using type = T; 
};

template<class T>
struct enable_if<false, T>
{
    ;
};

template<bool Val, class T>
using enable_if_t = typename enable_if<Val, T>::type;

由此可见,如果Val是true, 这个模板就是T类型,否则会编译错误(不存在类型T)。

enable_if_t<true, int> a = 1; // ok, int a = 1
enable_if_t<false, int> a = 1; // error, <error-type> a = 1, there are no member 'type' in class enable_if

why

那么为什么需要这种东西呢?它解决了什么问题?

如果我们想要这样的代码,实现编译期计算一个类型是整数,浮点数还是其他,那么我们就可以写出这样的代码

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;

我们可以通过以下方式获取

constexpr auto type = get_type_v<int>; // NumType::INTEGER

problem

有一个问题我们没有提及,我们现在可以用enable_if_t判断当前类型和它包含的类型,但是如果我们要判断一个类型是否含有某个方法呢?

比如我们可以用下列代码求数组的大小,如果有Size方法就调用它,如果没有就是C-Style的数组的求法。

template<class T, size_t Siz>
size_t get_size(const T(&)[Siz])
{
    return Siz;
}

template<class T>
size_t get_size(T&& t, decltype(t.size())* = nullptr)
{
    return t.size();
}

int main()
{
    int a[3]{ 1, 2, 3 };
    get_size(a); // 3
    get_size(vector{ 1, 2, 3 }); // 3
    get_size(array{ 1, 2, 3 }); // 3
}

如上图所示,我们判断一个类有没有某个方法是麻烦的,因为不能通过简单方式判断一个表达式是否合法(不能简单转换为Bool类型)。

如果我们可以通过某种办法判断一群表达式的合法性就好了。

C++17: void_t

what

定义很简单,就是

template<class...>
using void_t = void;

好像就把一大堆类型转化成void了?

why

我们在enable_if_t章节最后说的那个问题就要搬到这里解决了,代码可以优化为

template<class T, size_t Siz>
size_t get_size(const T(&)[Siz])
{
    return Siz;
}

template<class T, class = void_t<decltype(declval<T>().size())>>
size_t get_size(T&& t)
{
    return t.size();
}

易错

在这里有些新手可能会写成这样

template<class T, void_t<decltype(declval<T>().size())>* = nullptr>
size_t get_size(T&& t)
{
    return t.size();
}

或者

template<class T, add_pointer_t<void_t<decltype(declval<T>().size())>> = nullptr>
size_t get_size(T&& t)
{
    return t.size();
}

这里请注意,C++规定非类型模板无法进行SFINAE,因为非类型模板参数的默认值不属于模板参数推导过程的一部分。在模板实例化期间,编译器会根据函数调用中的实参来推导出模板参数的类型或值。但是,非类型模板参数的默认值并不参与这个过程,因此无法用于SFINAE。

但是,不参与SFINAE的非类型模板有默认值是被允许的。下面这个N只被赋予简单的值而不参与禁用当前模板。foo函数并没有试图使用非类型模板参数的默认值来进行SFINAE。它只是简单地为非类型模板参数提供了一个默认值,并没有试图使用它来启用或禁用模板。

template <int N = 12>
void foo() {
    return;
}

所以之前的问题是,第二个模板试图使用非类型模板参数的默认值nullptr来进行SFINAE。然而,根据C++标准,非类型模板参数的默认值不能用于SFINAE。这意味着,即使为非类型模板参数提供了默认值,它也不能用于启用或禁用模板,因此之前代码是错误的。

相似的,如果我们判断一个类是否有to_string函数,我们只需要这样做:

template<class Ty, class = void>
struct has_to_string : false_type{};

template<class Ty>
struct has_to_string<Ty, void_t<decltype(declval<Ty>().to_string())>> : true_type{};

template< class Ty >
inline constexpr auto has_to_string_v = has_to_string<Ty>::value;

综合以上,可以写出这样的代码(为了防止多个函数匹配)

#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();
}

example

判断一个类型是否为STL的智能指针

template <typename T, typename = void>
struct is_smart_pointer : std::false_type
{
};

template <typename T>
struct is_smart_pointer<T,
        std::void_t<decltype(std::declval<T>().operator ->()),
                    decltype(std::declval<T>().get())>> : std::true_type
{
};

template<typename T>
inline constexpr auto is_smart_pointer_v = is_smart_pointer<T>::value;