SNIFE 和 std::enable_if

本文介绍了模板元编程的基础概念,涵盖typename和using的区别,类型与struct的关系,以及SFINAE原理和std::enable_if的应用实例,展示了如何利用这些技术进行类型特化和条件编译。
摘要由CSDN通过智能技术生成

类型基础

在回顾模板之前,需要明确一个概念:模板编程是针对类型的计算。。这和我们平时的代码不同,我们平时写的程序都是针对数据的。

在模板元编程中,typename 用于定义类型;using 用于给模板类型赋值,注意这里的赋值和变量的赋值意义不同。参考下面的代码:

#include <iostream>

using LL = long long;

struct Foo {
    using T1 = int;
    using T2 = float;
};

int main() {
    LL n = 1e10;
    std::cout << n << std::endl;
    Foo::T1 a = 10;
    Foo::T2 b = 100.0;
    std::cout << a << ", " << b << std::endl;
    std::cout << sizeof(Foo) << std::endl;  // 类型不占用结构体的size
    return 0;
}

/*
10000000000
10, 100
1
*/

模板一般和 struct 共同使用,类型不占用结构体的size。类型的可见性和成员变量的可见性一致,比如:

#include <iostream>
class Foo {
    using T = int;
public:
    static void foo() {
        T a = 10;  // 这里可用,非 static 同样可用 T
        std::cout << a << std::endl;
    }
};

int main() {
    // Foo::T a = 10;   // private 函数不可用
    Foo::foo();
    return 0;
}

根绝这个例子,我们可以把类型认为是不占用空间的成员。因此,如果是继承关系,那么子类会继承父类的类型,可见性和成员变量的一致。代码实例:

#include <iostream>

struct Base {
    using T1 = int;
};

struct Derived: Base {
    using T2 = float;
};

int main() {
    Derived::T1 a = 10;
    Derived::T2 b = 1.1;
    std::cout << a << ", " << b << std::endl;
    return 0;
}

之后,我们再看typename 关键字,用于定义类型:

#include <iostream>

template<typename T>
struct Foo {
    T a;
};

int main() {
    Foo<int> foo{};
    foo.a = 10;
    std::cout << foo.a << std::endl;
    return 0;
}

对于这种情况,class 也可以胜任。但是typename有个更特殊的功能,用于声明类型:

#include <iostream>

template<typename T>
struct Foo {
    void foo() {
        typename T::t a = 10;  // T::t显式说明这是类型(1)
        std::cout << a << std::endl;
    }
};

struct Type {
    using t = int;
};

int main() {
    Foo<Type> f{};
    f.foo();

    Foo<int> f1{};  // 这里声明不会报错(2)
    // f1.foo();  // 这里会报错(3)
    return 0;
}

上面的代码(1)中,typename 显式说明 T::t是类型。因为,T::t 也可以作为成员函数或者成员变量:

#include <iostream>

template<typename T>
struct Foo {
    void foo() {
        T::t();  // 这里作为成员变量了
    }
};

struct Type {
    static void t() {
        std::cout << "type test" << std::endl;
    }
};

int main() {
    Foo<Type> foo{};
    foo.foo();
    return 0;
}

SFINAE

Substitution Faile Is Not An Error

C++在执行模板匹配的时候,如果有多个候选项,那么当一个匹配不上的时候,不会报错,而是会继续匹配其他的,直到有最佳的匹配项,或者都匹配不上报错为止。

当然,也有优先匹配的原则,这个就是模板偏化和特化,这里不再特殊介绍。

直接给出一个 SFINAE 的例子:

#include <iostream>
#include <type_traits>

template<typename T>
class Future {};

template<typename T>
struct IsFuture: std::false_type {};

template<typename T>
struct IsFuture<Future<T>>: std::true_type {};

int main() {
    // 这里匹配到 Future<T>,所以类型是 True
    std::cout << IsFuture<Future<int>>::value << std::endl;
    // 匹配不到Future,会去默认的
    std::cout << IsFuture<int>::value << std::endl;
    return 0;
}

std::enable_if

SFNIAE 最经典的例子,就是std::enable_if 了,具体用法看手册,给出一个简单的例子:

#include <iostream>
#include <type_traits>

template<typename T>
std::enable_if_t<
        std::is_same_v<T, int>,
        double> foo(T n) {
    return n * 2.0;
}

template<typename T>
std::enable_if_t<
        std::is_same_v<T, double>,
        int> foo(T n) {
    return int(n) % 2;
}

int main() {
    std::cout << foo(10) << std::endl;  // --> 20
    std::cout << foo(1.5) << std::endl;  // --> 1
    // std::cout << foo("aaa") << std::endl;  // (1)
    return 0;
}

(1)没有对应的定义,无法生成定义。

上面是个简单的例子,当然了,这个通过函数重载也可以做。我们给出一个无法通过函数重载做的例子:

#include <iostream>
#include <type_traits>

template<typename F, typename ...Args>
std::enable_if_t<
        std::is_same_v<std::invoke_result_t<F, Args...>, int>,
        double> foo(F &&f, Args &&... args) {
    int n = std::forward<F>(f)(std::forward<Args>(args)...);
    return n * 2.0;
}

template<typename F, typename ...Args>
std::enable_if_t<
        std::is_same_v<std::invoke_result_t<F, Args...>, double>,
        int> foo(F &&f, Args &&... args) {
    double n = std::forward<F>(f)(std::forward<Args>(args)...);
    return int(n) % 2;
}

int f(int, double) {
    return 10;
}

double f1(double, int) {
    return 3.0;
}

void f2() {}

int main() {
    int a = 10;
    std::cout << foo(f, a, 1.0) << std::endl;
    std::cout << foo(f1, 10.0, a) << std::endl;
    // std::cout << foo(f2) << std::endl;  // 错误,没有这种类型重载
    return 0;
}

意义是,foo 函数的参数是一个函数ff对应的值,之后根据f的返回值来特化出不同的版本。这种例子,是函数重载根本无法实现的。

参考文档

http://kaiyuan.me/2018/05/08/sfinae/
https://marvinsblog.net/post/2019-09-11-cpp-sfinae-intro/
https://zhuanlan.zhihu.com/p/21314708

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值