现代c++中的模板元编程和函数式

模板元编程

  1. 模板最重要的特点:惰性、多次编译。

  2. 模板的定义

    #include <iostream>
    
    template <class T>
    T twice(T t) {
        return t * 2;
    }
    int main() {
        std::cout << twice<int>(21) << std::endl;
        std::cout << twice<float>(3.14f) << std::endl;
        std::cout << twice<double>(2.718) << std::endl;
    }
    
    
  3. 模板函数 – 类型自动推导和特化

    #include <iostream>
    
    template <class T>
    T twice(T t) {
        return t * 2;
    }
    std::string twice(std::string t) {
        return t + t;
    }
    int main() {
        std::cout << twice(21) << std::endl;
        std::cout << twice(3.14f) << std::endl;
        std::cout << twice(2.718) << std::endl;
        std::cout << twice(std::string("hello")) << std::endl;
        std::cout << twice("hello") << std::endl;  //T 会推导成const char *,从而导致出错
    }
    
  4. 默认参数类型

    #include <iostream>
    
    template <class T = int>
    T two() {
        return 2;
    }
    int main() {
        std::cout << two<int>() << std::endl;
        std::cout << two<float>() << std::endl;
        std::cout << two<double>() << std::endl;
        std::cout << two() << std::endl;  // 等价于 two<int>()
    }
    
  5. 整数可作为参数

    #include <iostream>
    
    template <int N>
    void show_times(std::string msg) {
        for (int i = 0; i < N; i++) {
            std::cout << msg << std::endl;
        }
    }
    int main() {
        show_times<1>("one");
        show_times<3>("three");
        show_times<4>("four");
    }
    
    不过模板参数只支持整数类型(包括 enum)。
    浮点类型、指针类型,不能声明为模板参数。自定义类型也不可以,比如:
    template <float F, glm::vec3 V>  // 错误
    
  6. 多个模板参数

    #include <iostream>
    
    template <int N = 1, class T>
    void show_times(T msg) {
        for (int i = 0; i < N; i++) {
            std::cout << msg << std::endl;
        }
    }
    
    int main() {
        show_times("one");
        show_times<3>(42);
        show_times<4>('%');
    }
    
  7. 参数部分特化

    #include <iostream>
    #include <vector>
    
    template <class T>
    T sum(std::vector<T> const &arr) {
        T res = 0;
        for (int i = 0; i < arr.size(); i++) {
            res += arr[i];
        }
        return res;
    }
    
    int main() {
        std::vector<int> a = {4, 3, 2, 1};
        std::cout << sum(a) << std::endl;
        std::vector<float> b = {3.14f, 2.718f};
        std::cout << sum(b) << std::endl;
    }
    
  8. 为什么要支持整数作为模板参数:因为是编译期常量

    模板只需要支持 class T 不就行了?反正 int N 可以作为函数的参数传入,模板还不支持浮点。

    • template<int N> 传入的 N,是一个编译期常量,每个不同的 N,编译器都会单独生成一份代码,从而可以对他做单独的优化。

    • 而 func(int N),则变成运行期常量,编译器无法自动优化,只能运行时根据被调用参数 N 的不同。

    • 比如 show_times<0>() 编译器就可以自动优化为一个空函数。因此模板元编程对高性能编程很重要。

      template <int N = 1, class T>
      void show_times(T msg) {
          for (int i = 0; i < N; i++) {
              std::cout << msg << std::endl;
          }
      }
      
    • 通常来说,模板的内部实现需要被暴露出来,除非使用特殊的手段,否则,定义和实现都必须放在头文件里。

    • 但也正因如此,如果过度使用模板,会导致生成的二进制文件大小剧增,编译变得很慢等。

  9. 模板的应用:编译期优化案例

    // 优化前:
    #include <iostream>
    int sumto(int n, bool debug) {
        int res = 0;
        for (int i = 1; i <= n; i++) {
            res += i;
            if (debug)
                std::cout << i << "-th: " << res << std::endl;
        }
        return res;
    }
    int main() {
        std::cout << sumto(4, true) << std::endl;
        std::cout << sumto(4, false) << std::endl;
        return 0;
    }
    
    // 优化后:
    #include <iostream>
    template <bool debug>
    int sumto(int n) {
        int res = 0;
        for (int i = 1; i <= n; i++) {
            res += i;
            if constexpr (debug)
                std::cout << i << "-th: " << res << std::endl;
        }
        return res;
    }
    int main() {
        std::cout << sumto<true>(4) << std::endl;
        std::cout << sumto<false>(4) << std::endl;
        return 0;
    }
    
  10. 模板的难题:编译期常量的限制

    编译期常量的限制就在于他不能通过运行时变量组成的表达式来指定。比如:if constexpr (i % 2)这种不能通过编译;

    #include <iostream>
    template <bool debug>
    int sumto(int n) {
        int res = 0;
        for (int i = 1; i <= n; i++) {
            res += i;
            if constexpr (debug)
                std::cout << i << "-th: " << res << std::endl;
        }
        return res;
    }
    

    除了 if constexpr 的表达式不能用运行时变量,模板尖括号内的参数也不能:

    int main() {
        bool debug = true;
        std::cout << sumto<debug>(4) << std::endl;
        return 0;
    }
    

    可以在 bool debug 变量的定义前面加上 constexpr 来解决:

    int main() {
        constexpr bool debug = true;
        // 但这样 debug = 右边的值也必须为编译期常量,否则出错:
        // constexpr bool debug = time(NULL); //ERROR
        std::cout << sumto<debug>(4) << std::endl;
        return 0;
    }
    

    编译期 constexpr 的表达式,一般是无法调用其他函数的,如果能保证函数 isnegative 里都可以在编译期求值,将他前面也标上 constexpr 即可。

    constexpr 函数不能调用 non-constexpr 函数。而且 constexpr 函数必须是内联(inline)的,不能分离声明和定义在另一个文件里。标准库的很多函数如 std::min 也是 constexpr 函数,可以放心大胆在模板尖括号内使用。

    constexpr bool isnegative(int n) {
        return n < 0;
    }
    
    int main() {
        constexpr bool debug = isnegative(-2014);
        std::cout << sumto<debug>(4) << std::endl;
        return 0;
    }
    
  11. 模板的惰性:延迟编译

    #include <iostream>
    
    template <class T = void>
    void func_that_never_pass_compile() {
        "字符串" = 2333;
    }
    
    int main() {
        return 0;
    }
    //没有出错,这是因为模板没有被调用,所以不会被实际编译!
    //而只有当 main 调用了这个函数,才会被编译,才会报错!
    
  12. 一个模板的例子

    #include <iostream>
    #include <vector>
    
    template <class T>
    void print(std::vector<T> const &a) {
        std::cout << "{";
        for (size_t i = 0; i < a.size(); i++) {
            std::cout << a[i];
            if (i != a.size() - 1)
                std::cout << ", ";
        }
        std::cout << "}" << std::endl;
    }
    int main() {
        std::vector<int> a = {1, 4, 2, 8, 5, 7};
        print(a);
        std::vector<double> b = {3.14, 2.718, 0.618};
        print(b);
        return 0;
    }
    
    #include <iostream>
    #include <vector>
    
    template <class T>
    std::ostream &operator<<(std::ostream &os, std::vector<T> const &a) {
        // 可以用 __PRETTY_FUNCTION__打印出当前函数的名字和参数,包括模板的参数
        os << __PRETTY_FUNCTION__ << std::endl;
        os << "{";
        for (size_t i = 0; i < a.size(); i++) {
            os << a[i];
            if (i != a.size() - 1)
                os << ", ";
        }
        os << "}";
        return os;
    }
    int main() {
        std::vector<int> a = {1, 4, 2, 8, 5, 7};
        std::cout << a << std::endl;
        std::vector<double> b = {3.14, 2.718, 0.618};
        std::cout << b << std::endl;
        return 0;
    }
    

自动类型推导

  1. 使用 auto 定义变量,其类型会自动根据等号右边的值来确定,注意,类成员不可以定义为 auto;

    #include <cstdio>
    #include <memory>
    
    struct MyClassWithVeryLongName {
    };
    
    
    int main() {
        auto p = std::make_shared<MyClassWithVeryLongName>();
    }
    
  2. auto作为函数返回值

    使用 auto 以后,会自动被推导为 return 右边的类型。
    不过也有三点注意事项:

    • 当函数有多条 return 语句时,所有语句的返回类型必须一致,否则 auto 会报错。
    • 当函数没有 return 语句时,auto 会被推导为 void。
    • 如果声明和实现分离了,则不能声明为 auto。比如:auto func(); // 错误
    #include <cstdio>
    #include <memory>
    
    struct MyClassWithVeryLongName {
    };
    
    auto func() {
        return std::make_shared<MyClassWithVeryLongName>();
    }
    
    int main() {
        auto p = func();
    }
    
  3. 自动类型推导与引用

    #include <cstdio>
    
    int main() {
        int x = 233;
        auto &ref = x;
        auto const &ref1 = x;
    }
    

    函数返回引用:

    #include <cstdio>
    #include <string>
    #include <map>
    
    auto &product_table() {
        static std::map<std::string, int> instance;
        return instance;
    }
    
    int main() {
        product_table().emplace("佩奇", 80);
        product_table().emplace("妈妈", 100);
    }
    
  4. 一个类型查看工具

    #include <iostream>
    #include <cstdlib>
    #include <string>
    #if defined(__GNUC__) || defined(__clang__)
    #include <cxxabi.h>
    #endif
    
    template <class T>
    std::string cpp_type_name() {
        const char *name = typeid(T).name();
    #if defined(__GNUC__) || defined(__clang__)
        int status;
        char *p = abi::__cxa_demangle(name, 0, 0, &status);
        std::string s = p;
        std::free(p);
    #else
        std::string s = name;
    #endif
        if (std::is_const_v<std::remove_reference_t<T>>)
            s += " const";
        if (std::is_volatile_v<std::remove_reference_t<T>>)
            s += " volatile";
        if (std::is_lvalue_reference_v<T>)
            s += " &";
        if (std::is_rvalue_reference_v<T>)
            s += " &&";
        return s;
    }
    
    #define SHOW(T) std::cout << cpp_type_name<T>() << std::endl;
    
    int main() {
        SHOW(int);
        SHOW(const int &);
        typedef const float *const &MyType;
        SHOW(MyType);
    }
    
  5. 获取表达式的类型:decltype

    注:decltype是在编译期确定的类型,所以对程序执行的时间的影响较小。

    #include <iostream>
    #include <cstdlib>
    #include <string>
    #if defined(__GNUC__) || defined(__clang__)
    #include <cxxabi.h>
    #endif
    
    template <class T>
    std::string cpp_type_name() {
        const char *name = typeid(T).name();
    #if defined(__GNUC__) || defined(__clang__)
        int status;
        char *p = abi::__cxa_demangle(name, 0, 0, &status);
        std::string s = p;
        std::free(p);
    #else
        std::string s = name;
    #endif
        if (std::is_const_v<std::remove_reference_t<T>>)
            s += " const";
        if (std::is_volatile_v<std::remove_reference_t<T>>)
            s += " volatile";
        if (std::is_lvalue_reference_v<T>)
            s += " &";
        if (std::is_rvalue_reference_v<T>)
            s += " &&";
        return s;
    }
    
    #define SHOW(T) std::cout << cpp_type_name<T>() << std::endl;
    
    int main() {
        int a, *p;
        SHOW(decltype(3.14f + a));
        SHOW(decltype(42));
        SHOW(decltype(&a));
        SHOW(decltype(p[0]));
        SHOW(decltype('a'));
    
        SHOW(decltype(a));    // int
        SHOW(decltype((a)));  // int &
        // 后者由于额外套了层括号,所以变成了 decltype(表达式)
    }
    
  6. c++14引入的decltype(auto)

    这种用法的原因是使用auto p = func();时,p会被自动的decay掉;

    auto &p = func();时,func()函数的返回值就必须时引用的。

    #include <cstdio>
    
    int t;
    
    int &func_ref() {
        return t;
    }
    
    int const &func_cref() {
        return t;
    }
    
    int func_val() {
        return t;
    }
    
    int main() {
        decltype(auto) a = func_cref();  // int const &a
        // 等价于decltype(func_cref()) a = func_cref();
        decltype(auto) b = func_ref();   // int &b
        decltype(auto) c = func_val();   // int c
    }
    

    该用法在代理模式中,用于完美转发函数返回值。比如:

    decltype(auto) at(size_t i) const {
      return m_internal_class.at(i);
    }
    
  7. decltype的一个经典的使用例子:

    #include <iostream>
    #include <vector>
    
    template <class T1, class T2>
    auto add(std::vector<T1> const &a, std::vector<T2> const &b) {
        using T0 = decltype(T1{} + T2{});
        std::vector<T0> ret;
        for (size_t i = 0; i < std::min(a.size(), b.size()); i++) {
            ret.push_back(a[i] + b[i]);
        }
        return ret;
    }
    
    int main() {
        std::vector<int> a = {2, 3, 4};
        std::vector<float> b = {0.5f, 1.0f, 2.0f};
        auto c = add(a, b);
        for (size_t i = 0; i < c.size(); i++) {
            std::cout << c[i] << std::endl;
        }
        return 0;
    }
    
  8. 类型擦除的玩法

    int main(){
        std::cout << std::is_same_v<int const*,int *const> << std::endl;
        
        //std::remove_const_t移除const属性
        std::cout << std::is_same_v<std::remove_const_t<int const>,int> << std::endl; //1
        
        //std::remove_referenece_t移除const属性
        std::cout << std::is_same_v<std::remove_reference_t<std::remove_const_t<int const &>>,int> << std::endl; //1
        
        //std::decay_t去除const &属性,也能把int [] 退化为int *
        std::cout << std::is_same_v<std::dacay_t<int const&>,int> << std::endl; //1
        std::cout << std::is_same_v<std::dacay_t<int []>,int *> << std::endl; //1
    }
    
  9. using创建类型别名

    typedef std::vector<int> VecInt;
    using VecInt = std::vector<int>;
    //以上是等价的。
    
    typedef int (*PFunc)(int);
    using PFunc = int(*)(int);
    //以上是等价的。
    

函数式编程

  1. 函数作为参数,这种写法跟函数指针是一样的

    #include <cstdio>
    
    void say_hello() {
        printf("Hello!\n");
    }
    void call_twice(void func()) {
        func();
        func();
    }
    int main() {
        call_twice(say_hello);
        return 0;
    }
    
    #include <cstdio>
    
    void print_number(int n) {
        printf("Number %d\n", n);
    }
    void call_twice(void func(int)) {
        func(0);
        func(1);
    }
    int main() {
        call_twice(print_number);
        return 0;
    }
    
  2. 函数作为模板类型

    #include <cstdio>
    
    void print_float(float n) {
        printf("Float %f\n", n);
    }
    
    void print_int(int n) {
        printf("Int %d\n", n);
    }
    
    template <class Func>
    void call_twice(Func func) {
        func(0);
        func(1);
    }
    
    int main() {
        call_twice(print_float);
        call_twice(print_int);
        return 0;
    }
    
    #include <cstdio>
    
    template <class Func>
    void call_twice(Func func) {
        func(0);
        func(1);
    }
    
    int main() {
        auto myfunc = [] (int n) {
            printf("Number %d\n", n);
        };
        call_twice(myfunc);
        return 0;
    }
    
  3. lambda的返回值

    #include <iostream>
    
    template <class Func>
    void call_twice(Func func) {
        std::cout << func(0) << std::endl;
        std::cout << func(1) << std::endl;
    }
    
    int main() {
        auto twice = [] (int n) -> int {
            return n * 2;
        };
        call_twice(twice);
        
        auto twice = [] (int n) {  // 单一返回值类型可自动推导
            return n * 2;          // 返回类型自动推导为 int
        };
        call_twice(twice);
        return 0;
    }
    
    
  4. lambda表达式作为参数传递:传常引用避免拷贝开销

    #include <iostream>
    
    template <class Func>
    void call_twice(Func const &func) {
        std::cout << func(0) << std::endl;
        std::cout << func(1) << std::endl;
        std::cout << "Func 的大小: " << sizeof(Func) << std::endl;
    }
    
    int main() {
        int fac = 2;
        int counter = 0;
        auto twice = [&] (int n) {
            counter++;
            return n * fac;
        };
        call_twice(twice);
        std::cout << "调用了 " << counter << " 次" << std::endl;
        return 0;
    }
    
  5. lambda表达式作为返回值

    既然函数可以作为参数,当然也可以作为返回值!
    由于 lambda 表达式永远是个匿名类型,我们需要将 make_twice 的返回类型声明为 auto 让他自动推导。

    #include <iostream>
    
    template <class Func>
    void call_twice(Func const &func) {
        std::cout << func(0) << std::endl;
        std::cout << func(1) << std::endl;
        std::cout << "Func 大小: " << sizeof(Func) << std::endl;
    }
    
    auto make_twice(int fac) {
        //修改为return [=] (int n) {
        return [&] (int n) {
            return n * fac;
        };
    }
    
    int main() {
        auto twice = make_twice(2);
        call_twice(twice);
        return 0;
    }
    
    

    然而当我们试图用 [&] 捕获参数 fac 时,却出了问题;

    这是因为 [&] 捕获的是引用,是 fac 的地址,而 make_twice 已经返回了,导致 fac 的引用变成了内存中一块已经失效的地址。

    总之,如果用 [&],请保证 lambda 对象的生命周期不超过他捕获的所有引用的寿命

  6. lambda表达式:如何避免用模板参数

    虽然 <class Func> 这样可以让编译器对每个不同的 lambda 生成一次,有助于优化。

    但是有时候我们希望通过头文件的方式分离声明和实现,或者想加快编译,这时如果再用 template class 作为参数就不行了。

    为了灵活性,可以用 std::function 容器。

    只需在后面尖括号里写函数的返回类型和参数列表即可,比如:std::function<int(float, char *)>;

    但是这样会牺牲性能,std::function基于虚函数实现,有类似虚函数大小的开销。

    #include <iostream>
    #include <functional>
    
    void call_twice(std::function<int(int)> const &func) {
        std::cout << func(0) << std::endl;
        std::cout << func(1) << std::endl;
        std::cout << "Func 大小: " << sizeof(func) << std::endl;
    }
    
    std::function<int(int)> make_twice(int fac) {
        return [=] (int n) {
            return n * fac;
        };
    }
    
    int main() {
        auto twice = make_twice(2);
        call_twice(twice);
        return 0;
    }
    
  7. 无捕获的 lambda 可以传为函数指针

    如果你的 lambda 没有捕获任何局部变量,也就是 [],那么不需要用 std::function<int(int)>,直接用函数指针的类型 int(int) 或者 int(*)(int) 即可。

    函数指针效率更高一些,但是 [] 就没办法捕获局部变量了(全局变量还是可以的)。

    最大的好处是可以伺候一些只接受函数指针的 C 语言的 API 比如 pthread 和 atexit。

    #include <iostream>
    #include <functional>
    
    void call_twice(int func(int)) {
        std::cout << func(0) << std::endl;
        std::cout << func(1) << std::endl;
        std::cout << "Func 大小: " << sizeof(func) << std::endl;
    }
    
    int main() {
        call_twice([] (int n) {
            return n * 2;
        });
        return 0;
    }
    
    
  8. lambda+模板

    可以将 lambda 表达式的参数声明为 auto,声明为 auto 的参数会自动根据调用者给的参数推导类型,基本上和 template <class T> 等价。

    auto const & 也是同理,等价于模板函数的 T const &。

    带 auto 参数的 lambda 表达式,和模板函数一样,同样会有惰性、多次编译的特性。

    #include <iostream>
    #include <functional>
    
    template <class Func>
    void call_twice(Func const &func) {
        std::cout << func(3.14f) << std::endl;
        std::cout << func(1) << std::endl;
        std::cout << "Func 大小: " << sizeof(func) << std::endl;
    }
    
    int main() {
        call_twice([] (auto n) {
            return n * 2;
        });
        return 0;
    }
    
    /*等价于:
    template <class T>
    auto twice(T n){
    	return n * 2;
    }
    */
    
  9. lambda 用途举例:yield模式

    #include <iostream>
    #include <vector>
    
    template <class Func>
    void fetch_data(Func const &func) {
        for (int i = 0; i < 32; i++) {
            func(i);
            func(i + 0.5f);
        }
    }
    
    int main() {
        std::vector<int> res_i;
        std::vector<float> res_f;
        fetch_data([&] (auto const &x) {
            using T = std::decay_t<decltype(x)>;
            if constexpr (std::is_same_v<T, int>) {
                res_i.push_back(x);
            } else if constexpr (std::is_same_v<T, float>) {
                res_f.push_back(x);
            }
        });
        std::cout << res_i.size() << std::endl;
        std::cout << res_f.size() << std::endl;
        return 0;
    }
    
  10. lambda 用途举例:立即求值

    #include <iostream>
    #include <vector>
    
    int main() {
        std::vector<int> arr = {1, 4, 2, 8, 5, 7};
        int tofind = 5;
        int index = [&] {
            for (int i = 0; i < arr.size(); i++)
                if (arr[i] == tofind)
                    return i;
            return -1;
        }();
        std::cout << index << std::endl;
        return 0;
    }
    
  11. lambda 用途举例:局部实现递归

    #include <iostream>
    #include <vector>
    #include <set>
    
    int main() {
        std::vector<int> arr = {1, 4, 2, 8, 5, 7, 1, 4};
        std::set<int> visited;
        auto dfs = [&] (auto const &dfs, int index) -> void {
            if (visited.find(index) == visited.end()) {
                visited.insert(index);
                std::cout << index << std::endl;
                int next = arr[index];
                dfs(dfs, next);
            }
        };
        dfs(dfs, 0);
        return 0;
    }
    

常用新容器

tuple
  1. std::tuple<…> 可以将多个不同类型的值打包成一个。尖括号里填各个元素的类型。

    之后可以用 std::get<0> 获取第0个元素,std::get<1> 获取第1个元素,以此类推(从0开始数数)。

    #include <iostream>
    #include <tuple>
    
    int main() {
        auto tup = std::tuple<int, float, char>(3, 3.14f, 'h');
    
        int first = std::get<0>(tup);
        float second = std::get<1>(tup);
        char third = std::get<2>(tup);
    
        std::cout << first << std::endl;
        std::cout << second << std::endl;
        std::cout << third << std::endl;
        return 0;
    }
    

    当用于构造函数时,std::tuple<…> 尖括号里的类型可以省略,这是 C++17 的新特性:CTAD。
    此外,通过 auto 自动推导 get 的返回类型。

    #include <iostream>
    #include <tuple>
    
    int main() {
        auto tup = std::tuple(3, 3.14f, 'h');
    
        auto first = std::get<0>(tup);
        auto second = std::get<1>(tup);
        auto third = std::get<2>(tup);
    
        std::cout << first << std::endl;
        std::cout << second << std::endl;
        std::cout << third << std::endl;
        return 0;
    }
    
  2. tuple结构化绑定

    tuple需要一个个去 get 好麻烦,可以用结构化绑定的语法:
    auto [x, y, ...] = tup;
    利用一个方括号,里面是变量名列表,即可解包一个 tuple。里面的数据会按顺序赋值给每个变量,非常方便。

    #include <iostream>
    #include <tuple>
    
    int main() {
        auto tup = std::tuple(3, 3.14f, 'h');
    
        auto [first, second, third] = tup;
    
        std::cout << first << std::endl;
        std::cout << second << std::endl;
        std::cout << third << std::endl;
        return 0;
    }
    
  3. tuple结构化绑定为引用

    #include <iostream>
    #include <tuple>
    
    int main() {
        auto tup = std::tuple(3, 3.14f, 'h');
    
        auto &[first, second, third] = tup;
    
        std::cout << std::get<0>(tup) << std::endl;
        first = 42;
        std::cout << std::get<0>(tup) << std::endl;
    
        return 0;
    }
    //同理,通过 auto const & 绑定为常引用:
    //auto const &[x, y, ...] = tup;
    
  4. tuple:结构化绑定为万能推导

    不过要注意一下万能推导的 decltype(auto),由于历史原因,他对应的结构化绑定是 auto &&:

    #include <iostream>
    #include <tuple>
    
    int main() {
        auto tup = std::tuple(3, 3.14f, 'h');
    
        auto &&[first, second, third] = tup;
    
        std::cout << std::get<0>(tup) << std::endl;
        first = 42;
        std::cout << std::get<0>(tup) << std::endl;
    
        return 0;
    }
    
  5. 结构化绑定:还可以是任意自定义类

    结构化绑定不仅可以解包 std::tuple,还可以解包任意用户自定义类;
    配合打包的 {} 初始化表达式,真是太便利了!

    #include <iostream>
    #include <tuple>
    
    struct MyClass {
        int x;
        float y;
    };
    
    int main() {
        MyClass mc = {42, 3.14f};
    
        auto [x, y] = mc;
    
        std::cout << x << ", " << y << std::endl;
        return 0;
    }
    
  6. tuple用于函数多个返回值

    #include <iostream>
    #include <tuple>
    #include <cmath>
    
    std::tuple<bool, float> mysqrt(float x) {
        if (x >= 0.f) {
            return {true, std::sqrt(x)};
        } else {
            return {false, 0.0f};
        }
    }
    
    int main() {
        auto [success, value] = mysqrt(3.f);
        if (success) {
            printf("成功!结果为:%f\n", value);
        } else {
            printf("失败!找不到平方根!\n");
        }
        return 0;
    }
    
optional
  1. 有些函数,本来要返回 T 类型,但是有可能会失败!
    tuple作为返回值的例子中用 std::tuple<bool, T>,其中第一个 bool 表示成功与否。但是这样尽管失败了还是需要指定一个值 0.0f,非常麻烦。
    这种情况推荐用 std::optional<T>。
    成功时,直接返回 T。失败时,只需返回 std::nullopt 即可。

    #include <iostream>
    #include <optional>
    #include <cmath>
    
    std::optional<float> mysqrt(float x) {
        if (x >= 0.f) {
            return std::sqrt(x);
        } else {
            return std::nullopt;
        }
    }
    
    int main() {
        auto ret = mysqrt(-3.14f);
        // if(ret) {
        if (ret.has_value()) {
            printf("成功!结果为:%f\n", *ret);
            printf("成功!结果为:%f\n", ret.value());
        } else {
            printf("失败!找不到平方根!\n");
        }
        return 0;
    }
    
  2. optional:value() 会检测是否为空,空则抛出异常

    当 ret 没有值时(即 nullopt),ret.value() 会抛出一个异常,类型为std::bad_optional_access。

  3. optional:value_or() 方便地指定一个缺省值

    #include <iostream>
    #include <optional>
    #include <cmath>
    
    std::optional<float> mysqrt(float x) {
        if (x >= 0.f) {
            return std::sqrt(x);
        } else {
            return std::nullopt;
        }
    }
    
    int main() {
        auto ret = mysqrt(-3.14f);
        printf("成功!结果为:%f\n", ret.value_or(142857.f));
        return 0;
    }
    
  4. optional:operator*() 不检测是否为空,不会抛出异常

    • 除了 ret.value() 之外还可以用 *ret 获取 optional 容器中的值,不过他不会去检测是否 has_value(),也不会抛出异常,更加高效,但是要注意安全。

    • 请确保在 has_value() 的分支内使用 *ret,否则就是不安全的。

    • 如果 optional 里的类型是结构体,则也可以用 ret->xxx 来访问该结构体的属性。

  5. optional:operator bool() 和 has_value() 等价

    • 在 if 的条件表达式中,其实可以直接写 if (ret),他和 if (ret.has_value()) 等价。
    • 没错,这样看来 optional 是在模仿指针,nullopt 则模仿 nullptr。但是他更安全,且符合 RAII 思想,当设为 nullopt 时会自动释放内部的对象。
    • 利用这一点可以实现 RAII 容器的提前释放。和 unique_ptr 的区别在于他的对象存储在栈上,效率更高。
variant
  1. 安全的 union,存储多个不同类型的值

  2. 有时候需要一个类型“要么存储 int,要么存储 float”,这时候就可以用 std::variant<int, float>。

    和 union 相比,variant 符合 RAII 思想,更加安全易用。

    给 variant 赋值只需用普通的 = 即可。

    variant 的特点是只存储其中一种类型。

    tuple 的特点是每个类型都有存储。

  3. 获取容器中的数据用 std::get

    要获取某个类型的值,比如要获取 int 用 std::get<int>。如果当前 variant 里不是这个类型,就会抛出异常:std::bad_variant_access。

    此外,还可以通过 std::get<0> 获取 variant 列表中第 0 个类型,这个例子中和 std::get<int> 是等价的。

    #include <iostream>
    #include <variant>
    
    int main() {
        std::variant<int, float> v = 3;
    
        std::cout << std::get<int>(v) << std::endl;   // 3
        std::cout << std::get<0>(v) << std::endl;     // 3
    
        v = 3.14f;
    
        std::cout << std::get<float>(v) << std::endl; // 3.14f
        std::cout << std::get<int>(v) << std::endl;   // 运行时错误
    
        return 0;
    }
    
  4. 判断当前是哪个类型用 std::holds_alternative

    可以用 std::holds_alternative<int> 判断当前里面存储的是不是 int。

    #include <iostream>
    #include <variant>
    
    void print(std::variant<int, float> const &v) {
        if (std::holds_alternative<int>(v)) {
            std::cout << std::get<int>(v) << std::endl;
        } else if (std::holds_alternative<float>(v)) {
            std::cout << std::get<float>(v) << std::endl;
        }
    }
    
    int main() {
        std::variant<int, float> v = 3;
        print(v);
        v = 3.14f;
        print(v);
        return 0;
    }
    
  5. variant:判断当前是哪个类型用 v.index()

    #include <iostream>
    #include <variant>
    
    void print(std::variant<int, float> const &v) {
        if (v.index() == 0) {
            std::cout << std::get<0>(v) << std::endl;
        } else if (v.index() == 1) {
            std::cout << std::get<1>(v) << std::endl;
        }
    }
    
    int main() {
        std::variant<int, float> v = 3;
        print(v);
        v = 3.14f;
        print(v);
        return 0;
    }
    
  6. variant:批量匹配 std::visit

    • 如果你的 if-else 每个分支长得都差不多(除了 std::get<> 的类型不一样以外),可以考虑用 std::visit,他会自动用相应的类型,调用你的 lambda,lambda 中往往是个重载函数。
    • 这里用到了带 auto 的 lambda,利用了他具有多次编译的特性,实现编译多个分支的效果。
      std::visit、std::variant 的这种模式称为静态多态,和虚函数、抽象类的动态多态相对.
    • 静态多态的优点是:性能开销小,存储大小固定。缺点是:类型固定,不能运行时扩充。
    #include <iostream>
    #include <variant>
    
    void print(std::variant<int, float> const &v) {
        std::visit([&] (auto const &t) {
            std::cout << t << std::endl;
        }, v);
    }
    
    int main() {
        std::variant<int, float> v = 3;
        print(v);
        v = 3.14f;
        print(v);
        return 0;
    }
    
  7. std::visit:还支持多个参数

    • 其实还可以有多个 variant 作为参数。
    • 相应地 lambda 的参数数量要与之匹配。
    • std::visit 会自动罗列出所有的排列组合!
    • 所以如果 variant 有 n 个类型,那 lambda 就要被编译 n² 次,编译可能会变慢。
    • 但是标准库能保证运行时是 O(1) 的(他们用函数指针实现分支,不是暴力 if-else)。
    #include <iostream>
    #include <variant>
    
    void print(std::variant<int, float> const &v) {
        std::visit([&] (auto const &t) {
            std::cout << t << std::endl;
        }, v);
    }
    
    auto add(std::variant<int, float> const &v1,
             std::variant<int, float> const &v2) {
        std::variant<int, float> ret;
        std::visit([&] (auto const &t1, auto const &t2) {
            ret = t1 + t2;
        }, v1, v2);
        return ret;
    }
    
    int main() {
        std::variant<int, float> v = 3;
        print(add(v, 3.14f));
        return 0;
    }
    
  8. std::visit:可以有返回值

    std::visit里面的 lambda 可以有返回值,不过都得同样类型。
    利用这一点进一步优化:

    #include <iostream>
    #include <variant>
    
    void print(std::variant<int, float> const &v) {
        std::visit([&] (auto const &t) {
            std::cout << t << std::endl;
        }, v);
    }
    
    auto add(std::variant<int, float> const &v1, std::variant<int, float> const &v2) {
        return std::visit([&] (auto const &t1, auto const &t2)
                   -> std::variant<int, float> {
            return t1 + t2;
        }, v1, v2);
    }
    
    int main() {
        std::variant<int, float> v = 3;
        print(add(v, 3.14f));
        return 0;
    }
    
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值