C++07_std::tuple、std::optional、std::variant、std::visit

std::tuple

std::tuple

常用容器:tuple

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 的返回类型。

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);
    return 0;
}

结构化绑定

可是需要一个个去 get 还是好麻烦。没关系,可以用结构化绑定的语法:auto [x, y, ...] = tup;

利用一个方括号,里面是变量名列表,即可解包一个 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;
}

结构化绑定为引用

结构化绑定也支持绑定为引用:auto &[x, y, ...] = tup;这样相当于解包出来的 x, y, ... 都是 auto & 推导出来的引用类型。对引用的修改可以影响到原 tuple 内的值。

同理,通过 auto const & 绑定为常引用:auto const &[x, y, ...] = tup;常引用虽然不能修改,但是可以避免一次不必要拷贝。

结构化绑定:还可以是任意自定义类

其实,结构化绑定不仅可以解包 std::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;
}

配合打包的 {} 初始化表达式,真是太便利了!惊不惊喜?意不意外?可惜 std::get 并不支持自定义类。

用于函数多个返回值

std::tuple 可以用于有多个返回值的函数。

如上一讲中所说,当函数返回值确定时,return 可以用 {} 表达式初始化,不必重复写前面的类名 std::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) {
        std::cout << "success! result: " << value << std::endl;
    } else {
        std::cout << "failure! result: " << value << std::endl;
    }
    return 0;
}

std::optional

std::optional

常用容器:optional

有些函数,本来要返回 T 类型,但是有可能会失败!

上个例子中用 std::tuple<bool, T>,其中第一个 bool 表示成功与否。但是这样尽管失败了还是需要指定一个值 0.0f,非常麻烦。

这种情况推荐用 std::optional

成功时,直接返回 T。失败时,只需返回 std::nullopt 即可。

#include <iostream>
#include <cmath>
#include <optional>

std::optional<float> mysqrt(float x) {
    if (x >= 0.f) {
        return std::sqrt(x);
    } else {
        return std::nullopt;
    }
}

int main() {
    auto ret = mysqrt(3.f);
    if (ret.has_value()) {
        std::cout << "success! result: " << value << std::endl;
    } else {
        std::cout << "failure! result: " << value << std::endl;
    }
    return 0;
}

value_or() 方便地指定一个缺省值

ret.value_or(3) 等价于:ret.has_value() ? ret.value() : 3

value() 会检测是否为空,空则抛出异常

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

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);
    std::cout << "success! result: " << ret.value() << std::endl;
    return 0;
}

operator*() 不检测是否为空,不会抛出异常

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

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

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

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.has_value()) {
        std::cout << "success! result: " << *ret << std::endl;
    } else {
        std::cout << "failure!" << std::endl;
    }
    return 0;
}

operator bool() 和 has_value() 等价

在 if 的条件表达式中,其实可以直接写 if (ret),他和 if (ret.has_value()) 等价。

没错,这样看来 optional 是在模仿指针,nullopt 则模仿 nullptr。但是他更安全,且符合 RAII 思想,当设为 nullopt 时会自动释放内部的对象。

利用这一点可以实现 RAII 容器的提前释放。和 unique_ptr 的区别在于他的对象存储在栈上,效率更高。

int main() {
    auto ret = mysqrt(3.14f);
    if (ret) {
        std::cout << "success! result: " << *ret << std::endl;
    } else {
        std::cout << "failure!" << std::endl;
    }
    return 0;
}

std::variant、std::visit

std::variant、std::visit

variant:安全的 union,存储多个不同类型的值

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

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

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

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

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

请区分,根据实际情况选用适当的容器。

#include <variant>

int main() {
    std::variant<int ,float> v = 3;
    v = 3.14f;
    return 0;
}

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

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

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

int main() {
    std::variant<int ,float> v = 3;
    std::cout << std::get<int>(v) << std::endl;
    std::cout << std::get<0>(v) << std::endl;

    v = 3.14f;
    std::cout << std::get<float>(v) << std::endl;
    std::cout << std::get<1>(v) << std::endl;
    return 0;
}

判断当前是哪个类型用 std::holds_alternative

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

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

判断当前是哪个类型用 v.index()

除了这个之外,还可以用成员方法 index() 获取当前是参数列表中的第几个类型。这样也可以实现判断。

void print(std::variant<int, float> const& v) {
    if (v.index() == 0) {
        std::cout << std::get<int>(v) << std::endl;
    } else if (v.index() == 1) {
        std::cout << std::get<float>(v) << std::endl;
    }
}

批量匹配 std::visit

如果你的 if-else 每个分支长得都差不多(除了 std::get<> 的类型不一样以外),可以考虑用 std::visit,他会自动用相应的类型,调用你的 lambda,lambda 中往往是个重载函数。

这里用到了带 auto 的 lambda,利用了他具有多次编译的特性,实现编译多个分支的效果。

std::visit、std::variant 的这种模式称为静态多态,和虚函数、抽象类的动态多态相对。

静态多态的优点是:性能开销小,存储大小固定。缺点是:类型固定,不能运行时扩充。

void print(std::variant<int, float> const& v) {
    std::visit([&] (auto const& t) {
        std::cout << t << std::endl;
    }, v);
}

std::visit:还支持多个参数

其实还可以有多个 variant 作为参数。

相应地 lambda 的参数数量要与之匹配。

std::visit 会自动罗列出所有的排列组合!

所以如果 variant 有 n 个类型,那 lambda 就要被编译 n² 次,编译可能会变慢。

但是标准库能保证运行时是 O(1) 的(他们用函数指针实现分支,不是暴力 if-else)。

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

std::visit:可以有返回值

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

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

Reference:

posted @ 2022-09-20 23:54  吹不散的流云  阅读(309)  评论(1编辑  收藏  举报