c++ - valid use of CRTP std::variant with two elements, but no more

Viewed 104

I'm trying to use CRTP with std::variant to achieve a classic Expression class, for example in some algebraic data type

data Expr = Num Int
     | Add Expr Expr
     | Sub Expr Expr
     ...

I use CRTP to avoid virtual methods

template <typename Impl>
struct Expr {
    int eval() { return static_cast<Impl *>(this)->eval_impl(); }
};

and I choose std::variant to store a the subexpressions in Add and Sub

// forward declarations
using expr_variant = std::variant<Num, Add, Sub>;

struct Num : Expr<Num> {
    int eval_impl() const { return n; }
    Num(int n) : n(n) {}
    int n;
};
struct Add : Expr<Add>
{
    std::shared_ptr<expr_variant> l, r;
    Add(const expr_variant &l, const expr_variant &r) : l{std::make_shared<expr_variant>(l)}, r{std::make_shared<expr_variant>(r)} {}
    int eval_impl() const { return std::visit(visitor{}, *l) + std::visit(visitor{}, *r); }
};
struct Sub : Expr<Sub> {
    std::shared_ptr<expr_variant> l, r;
    Sub(const expr_variant &l, const expr_variant &r) : l{std::make_shared<expr_variant>(l)}, r{std::make_shared<expr_variant>(r)} {}
    int eval_impl() const { return std::visit(visitor{}, *l) - std::visit(visitor{}, *r); }
};

the question is, it doesn't compile (saying that Sub isn't complete type and many other things), but it did work with only two of the variants (Num and Add). I know the code looks strange, it'd be appreciated if you can explain how to implement the above ADT in a cleaner way (probably without vtables).

the full code:

#include <variant>
#include <memory>

template <typename Impl>
struct Expr {
    int eval() { return static_cast<Impl *>(this)->eval_impl(); }
};
struct Add;
struct Sub;
struct Num;
struct visitor {
    int operator()(auto &e) const { return e.eval(); }
};
using expr_variant = std::variant<Num, Add, Sub>;

struct Num : Expr<Num> {
    int eval_impl() const { return n; }
    Num(int n) : n(n) {}
    int n;
};
struct Add : Expr<Add>
{
    std::shared_ptr<expr_variant> l, r;
    Add(const expr_variant &l, const expr_variant &r) : l{std::make_shared<expr_variant>(l)}, r{std::make_shared<expr_variant>(r)} {}
    int eval_impl() const { return std::visit(visitor{}, *l) + std::visit(visitor{}, *r); }
};
struct Sub : Expr<Sub> {
    std::shared_ptr<expr_variant> l, r;
    Sub(const expr_variant &l, const expr_variant &r) : l{std::make_shared<expr_variant>(l)}, r{std::make_shared<expr_variant>(r)} {}
    int eval_impl() const { return std::visit(visitor{}, *l) - std::visit(visitor{}, *r); }
};

int main()
{
    Sub e{Add{Num(45600), Num{70}}, Num{9}};
    return 0;
}
1 Answers

std::variant can't be used with incomplete types. When you have an inline definition of Add::eval_impl in the body of struct Add, Num is complete and Add is complete, but Sub hasn't been defined (which is why it works when you only have 2 types).

Move the definitions of these member functions that use the variant to after all the classes are complete:

using expr_variant = std::variant<Num, Add, Sub>;

struct Num : Expr<Num> {
    inline int eval_impl() const;
    Num(int n) : n(n) {}
    int n;
};
struct Add : Expr<Add>
{
    std::shared_ptr<expr_variant> l, r;
    Add(const expr_variant &l, const expr_variant &r) : l{std::make_shared<expr_variant>(l)}, r{std::make_shared<expr_variant>(r)} {}
    inline int eval_impl() const;
};
struct Sub : Expr<Sub> {
    std::shared_ptr<expr_variant> l, r;
    Sub(const expr_variant &l, const expr_variant &r) : l{std::make_shared<expr_variant>(l)}, r{std::make_shared<expr_variant>(r)} {}
    inline int eval_impl() const;
};

// All classes are now complete
int Num::eval_impl() const {
    return n;  // (This could still be in-class, but for consistency)
}
int Add::eval_impl() const {
    return std::visit(visitor{}, *l) + std::visit(visitor{}, *r);
}
int Sub::eval_impl() const {
    return std::visit(visitor{}, *l) - std::visit(visitor{}, *r);
}
Related