C++ variant实现多态:替代继承的新方案 | 最佳实践

C++ variant实现多态:替代继承的新方案 #

引言 #

C++17引入的variant为实现多态提供了一种新的思路。相比传统的继承方式,variant具有更好的类型安全性和运行时效率。本文将深入探讨如何使用variant实现多态,以及它与传统继承方式的优劣对比。

variant是什么? #

variant 这货类似于 union,可以存放多种类型的数据,但任何时刻最多只能存放其中一种类型。

这里大家可能有些疑问,既然有了 union,那为啥还要引入 variant 呢?

那肯定是因为 union 有缺点呗。

看这段 union 的基本用法:

union MyUnion {
    int a;
    float b;
    double c;
};

void test_simple_union() {
    MyUnion u;
    u.a = 1;
    std::cout << u.a << "\n";

    u.b = 1.32f;
    std::cout << u.b << "\n";

    u.c = 2.32;
    std::cout << u.c << "\n";
}

union 暴露了一个很重要的问题:无法获取当前存储的数据类型。比如当前存储的是 float,却按 int 方式获取,就会出错。

再看一段代码:

struct A {
    A() = default;
    A(int aa) : a{aa} { std::cout << "A() \n"; }
    ~A() { std::cout << "~A() \n"; }
    int a;
};

struct B {
    B() = default;
    B(float bb) : b{bb} { std::cout << "B() \n"; }
    ~B() { std::cout << "~B() \n"; }
    float b;
};

union MyStructUnion {
    A a;
    B b;

    ~MyStructUnion() { std::cout << "~MyStructUnion() \n"; }
};

void test_struct_union() {
    MyStructUnion u;

    new (&u.a) A(1);
    std::cout << u.a.a << "\n";
    u.a.~A();

    u.b = B(2.3f);
    std::cout << u.b.b << "\n";
    u.b.~B();
}

这里可以看到,union 无法自动处理构造和析构等逻辑,需要用户手动调用相关函数,存储自定义类型时特别麻烦。

所以,variant 诞生了:

struct C {
    C() = default;
    C(std::string cc) : c{cc} { std::cout << "C() \n"; }
    ~C() { std::cout << "~C() \n"; }
    std::string c;
};

void test_variant() {
    std::variant<std::monostate, A, C> u;
    u = 1;
    std::cout << std::get<A>(u).a << "\n";
    u = std::string("dsd");
    std::cout << std::get<C>(u).c << "\n";
}

使用 variant 完全不需要手动调用构造和析构函数,它会自动处理好所有逻辑,非常方便。

如何确定 variant 中当前存放的数据类型? #

variant 有一个 index() 方法可以做到。

看这段代码:

void test_index() {
    std::variant<std::monostate, int, float, std::string> var;
    var = 1;
    std::cout << var.index() << "\n"; // 1
    var = 2.90f;
    std::cout << var.index() << "\n"; // 2
    var = std::string("hello world");
    std::cout << var.index() << "\n"; // 3
}

通过 index() 方法,我们可以动态获取当前 variant 内部数据的类型索引。

为了更方便地使用,可以结合可变参数模板和模板元编程:

template <typename T, typename>
struct get_index;

template <size_t I, typename... Ts>
struct get_index_impl {};

template <size_t I, typename... Ts>
struct get_index_impl<I, T, T, Ts...> : std::integral_constant<size_t, I> {};

template <size_t I, typename T, typename... Ts>
struct get_index_impl<I, T, U, Ts...> : get_index_impl<I + 1, T, Ts...> {};

template <typename T, typename... Ts>
struct get_index<T, std::variant<Ts...>> : get_index_impl<0, T, Ts...> {};

template <typename T, typename... Ts>
constexpr auto get_index_v = get_index<T, Ts...>::value;

using variant_t = std::variant<std::monostate, int, float, std::string>;

constexpr static auto kPlaceholderIndex = get_index_v<std::monostate, variant_t>;
constexpr static auto kIntIndex = get_index_v<int, variant_t>;
constexpr static auto kFloatIndex = get_index_v<float, variant_t>;
constexpr static auto kStringIndex = get_index_v<std::string, variant_t>;

通过 get_index_v,可以方便地获取数据类型在 variant 中的索引。

如何用 variant 实现多态? #

可以使用 std::visit 搭配 variant 来实现多态。

看这段代码:

struct Visitor {
    void operator()(int i) const { std::cout << "int " << i << "\n"; }
    void operator()(float f) const { std::cout << "float " << f << "\n"; }
    void operator()(std::string s) const { std::cout << "string " << s << "\n"; }
};

void test_visitor_functor() {
    std::variant<int, float, std::string> var;
    var = 1;
    std::visit(Visitor(), var);
    var = 2.90f;
    std::visit(Visitor(), var);
    var = std::string("hello world");
    std::visit(Visitor(), var);
}

visit 内部会自动判断当前 variant 内部存储的类型,进而触发不同的行为。

使用 lambda 表达式更方便:

void test_visitor_lambda() {
    std::variant<int, float, std::string> var;
    var = 1;
    std::visit([](const auto& value) { std::cout << "value " << value << "\n"; }, var);
    var = 2.90f;
    std::visit([](const auto& value) { std::cout << "value " << value << "\n"; }, var);
    var = std::string("hello world");
    std::visit([](const auto& value) { std::cout << "value " << value << "\n"; }, var);
    var = std::string("hello type");
    std::visit(
        [](const auto& value) {
            using T = std::decay_t<decltype(value)>;
            if constexpr (std::is_same_v<T, int>) {
                std::cout << "int value " << value << "\n";
            } else if constexpr (std::is_same_v<T, float>) {
                std::cout << "float value " << value << "\n";
            } else if constexpr (std::is_same_v<T, std::string>) {
                std::cout << "string value " << value << "\n";
            }
        },
        var);
}

variant 为什么要搭配 monostate? #

普通的 variant 使用方法如下:

void test_variant() {
    std::variant<int, float> var;
    var = 12;
    std::cout << std::get<int>(var) << "\n";
    var = 12.1f;
    std::cout << std::get<float>(var) << "\n";
}

但如果存储自定义类型,需要加个 monostate,表示默认情况下它的存储类型就是 monostate。

struct S {
    S(int i) : value{i} {}
    int value;
};

void test_monostate() {
    std::variant<std::monostate, S> var;
    var = 12;
    std::cout << std::get<S>(var).value << "\n";
}

如何用 variant 实现多态? #

可以使用 std::visit 搭配 variant 来实现多态。

没有参数的多态 #

struct A {
    void func() const { std::cout << "func A \n"; }
};

struct B {
    void func() const { std::cout << "func B \n"; }
};

struct CallFunc {
    void operator()(const A& a) { a.func(); }
    void operator()(const B& b) { b.func(); }
};

void test_no_param_polymorphism() {
    std::variant<A, B> var;
    var = A();
    std::visit(CallFunc{}, var);
    var = B();
    std::visit(CallFunc{}, var);
}

带参数的多态 #

可以利用仿函数中的成员变量:

struct C {
    void func(int value) const { std::cout << "func C " << value << "\n"; }
};

struct D {
    void func(int value) const { std::cout << "func D " << value << "\n"; }
};

struct CallFuncParam {
    void operator()(const C& c) { c.func(value); }
    void operator()(const D& d) { d.func(value); }

    int value;
};

void test_param_polymorphism() {
    std::variant<C, D> var;
    var = C();
    std::visit(CallFuncParam{1}, var);
    var = D();
    std::visit(CallFuncParam{2}, var);
}

或者使用 lambda 表达式的捕获方式:

void test_param_lambda_polymorphism() {
    std::variant<C, D> var;
    int value = 1;
    auto caller = [&value](const auto& v) { v.func(value); };
    std::visit(caller, var);
    value = 2;
    std::visit(caller, var);
}

到这里已经介绍了 variant 实现多态的完整方案。

认为继承是个洪水猛兽的朋友,其实也可以考虑 variant 来实现多态的行为哈。

那同样是实现多态,是用继承好呢,还是用 variant 好呢?可以看这个图:

img

图片来源于这个链接:inheritance vs variant based polymorphism。大家感兴趣的可以直接移步哈。

另外大家应该也比较感兴趣 variant 是如何实现的。关于如何实现 variant,我找到了这篇文章,写的很不错,大家可以看看:C++11实现variant类型

参考链接 #

完整代码见:variant示例代码