4

I am writing a parser and am using a std::variant to represent the nodes of the AST. For basic expressions, I have something like:

struct Add;
struct Sub;
struct Mul;
struct Div;
typedef std::variant<Add, Sub, Mul, Div> Node;

(The actual structure definitions come later, because some of them refer to Node.)

I find this approach better than declaring Node as an abstract base class and Add etc. as subclasses because it enables me to separate the logic for using the AST from the AST itself. For example, the code that evaluates the expression can use std::visit without Node needing a virtual evaluate method or doing type checks and downcasts.

My problem it that there are some fields that I want every Node to have, and I'd like to be able to treat all Node variants the same when using those fields.

The only strategies I've come with are:

  • Define Node as a struct that has the common fields and a separate std::variant member.
  • Define the fields separately in each Node alternative, and for each field, define a visitor with a const auto & member that selects that field. In other words, just use visitors.

Is there any other way? What I'd really like to do is define an abstract base class with the fields, have all the Node alternatives (Add etc.) inherit from that class, and then be able to say, "I don't know what alternative this std::variant contains, but I know that they all are instances of this base class, and I just want to use it as an instance of that class.”

Willis Blackburn
  • 8,068
  • 19
  • 36
  • 2
    functor of `std::visit` doesn't need to have specific overloads for each alternative, if one overload cover several of them (as with base class), it is fine. – Jarod42 Dec 10 '19 at 13:57
  • 6
    What's stopping you from having all the classes inheriting from a common base class and then you have them all in a variant? If all the members of the variant inherit from the same class then it doesn't matter what object you inspect, you can reliably access those base class members. – NathanOliver Dec 10 '19 at 13:57
  • @Jarod42 If I use `std::visit`, it doesn't seem to matter if all the alternatives inherit from a base class or not. I modified Max's example so that all the alternatives inherited from `Base` and defined a visitor for `const Base &` and it generated the exact same dispatch logic as his example using `const auto &`. Which is consistent with MSalters's point that even if all alternatives inherit from a base class, they be treated identically. – Willis Blackburn Dec 10 '19 at 17:11
  • @NathanOliver-ReinstateMonica There's no way to use an `std::variant` without either knowing exactly which alternative it contains, or using a visitor to handle all the cases. Even if I know that all alternatives inherit from `Base`, I can't just get, say, alternative 0, and cast it to a `Base` reference. That would work with a union, but variants are tagged, and the code checks to make sure the variant actually contains the alternative you're trying to use. – Willis Blackburn Dec 10 '19 at 17:16
  • I meant: "...even if all alternatives inherit from a base class, they *cannot* be treated identically." – Willis Blackburn Dec 10 '19 at 17:18

2 Answers2

3

Assuming every type in the variant has some field std::string_view token; (whether this is written out in each one or comes from a common, not necessarily polymorphic, base class is up to you), you could write accessors like the following:

std::string_view getToken(const Node& node)
{
  return std::visit([](const auto& n) { return n.token; }, node);
}

https://godbolt.org/z/B8tAdY

Note that you don't have to manually add overloads - the auto in the lambda essentially makes the lambda a template, so you just rely on compile-time polymorphism. You end up with a nice jump table (each one just returning the appropriate member offsets):

std::__detail::__variant::__gen_vtable_impl<true, std::__detail::__variant::_Multi_array<std::basic_string_view<char, std::char_traits<char> > (*)(getToken(std::variant<Add, Sub, Mul, Div> const&)::$_0&&, std::variant<Add, Sub, Mul, Div> const&)>, std::tuple<std::variant<Add, Sub, Mul, Div> const&>, std::integer_sequence<unsigned long, 0ul> >::__visit_invoke(getToken(std::variant<Add, Sub, Mul, Div> const&)::$_0&&, std::variant<Add, Sub, Mul, Div> const&): # @"std::__detail::__variant::__gen_vtable_impl<true, std::__detail::__variant::_Multi_array<std::basic_string_view<char, std::char_traits<char> > (*)(getToken(std::variant<Add, Sub, Mul, Div> const&)::$_0&&, std::variant<Add, Sub, Mul, Div> const&)>, std::tuple<std::variant<Add, Sub, Mul, Div> const&>, std::integer_sequence<unsigned long, 0ul> >::__visit_invoke(getToken(std::variant<Add, Sub, Mul, Div> const&)::$_0&&, std::variant<Add, Sub, Mul, Div> const&)"
        mov     rax, qword ptr [rsi + 8]
        mov     rdx, qword ptr [rsi + 16]
        ret
std::__detail::__variant::__gen_vtable_impl<true, std::__detail::__variant::_Multi_array<std::basic_string_view<char, std::char_traits<char> > (*)(getToken(std::variant<Add, Sub, Mul, Div> const&)::$_0&&, std::variant<Add, Sub, Mul, Div> const&)>, std::tuple<std::variant<Add, Sub, Mul, Div> const&>, std::integer_sequence<unsigned long, 1ul> >::__visit_invoke(getToken(std::variant<Add, Sub, Mul, Div> const&)::$_0&&, std::variant<Add, Sub, Mul, Div> const&): # @"std::__detail::__variant::__gen_vtable_impl<true, std::__detail::__variant::_Multi_array<std::basic_string_view<char, std::char_traits<char> > (*)(getToken(std::variant<Add, Sub, Mul, Div> const&)::$_0&&, std::variant<Add, Sub, Mul, Div> const&)>, std::tuple<std::variant<Add, Sub, Mul, Div> const&>, std::integer_sequence<unsigned long, 1ul> >::__visit_invoke(getToken(std::variant<Add, Sub, Mul, Div> const&)::$_0&&, std::variant<Add, Sub, Mul, Div> const&)"
        mov     rax, qword ptr [rsi + 16]
        mov     rdx, qword ptr [rsi + 24]
        ret
std::__detail::__variant::__gen_vtable_impl<true, std::__detail::__variant::_Multi_array<std::basic_string_view<char, std::char_traits<char> > (*)(getToken(std::variant<Add, Sub, Mul, Div> const&)::$_0&&, std::variant<Add, Sub, Mul, Div> const&)>, std::tuple<std::variant<Add, Sub, Mul, Div> const&>, std::integer_sequence<unsigned long, 2ul> >::__visit_invoke(getToken(std::variant<Add, Sub, Mul, Div> const&)::$_0&&, std::variant<Add, Sub, Mul, Div> const&): # @"std::__detail::__variant::__gen_vtable_impl<true, std::__detail::__variant::_Multi_array<std::basic_string_view<char, std::char_traits<char> > (*)(getToken(std::variant<Add, Sub, Mul, Div> const&)::$_0&&, std::variant<Add, Sub, Mul, Div> const&)>, std::tuple<std::variant<Add, Sub, Mul, Div> const&>, std::integer_sequence<unsigned long, 2ul> >::__visit_invoke(getToken(std::variant<Add, Sub, Mul, Div> const&)::$_0&&, std::variant<Add, Sub, Mul, Div> const&)"
        mov     rax, qword ptr [rsi + 8]
        mov     rdx, qword ptr [rsi + 16]
        ret
std::__detail::__variant::__gen_vtable_impl<true, std::__detail::__variant::_Multi_array<std::basic_string_view<char, std::char_traits<char> > (*)(getToken(std::variant<Add, Sub, Mul, Div> const&)::$_0&&, std::variant<Add, Sub, Mul, Div> const&)>, std::tuple<std::variant<Add, Sub, Mul, Div> const&>, std::integer_sequence<unsigned long, 3ul> >::__visit_invoke(getToken(std::variant<Add, Sub, Mul, Div> const&)::$_0&&, std::variant<Add, Sub, Mul, Div> const&): # @"std::__detail::__variant::__gen_vtable_impl<true, std::__detail::__variant::_Multi_array<std::basic_string_view<char, std::char_traits<char> > (*)(getToken(std::variant<Add, Sub, Mul, Div> const&)::$_0&&, std::variant<Add, Sub, Mul, Div> const&)>, std::tuple<std::variant<Add, Sub, Mul, Div> const&>, std::integer_sequence<unsigned long, 3ul> >::__visit_invoke(getToken(std::variant<Add, Sub, Mul, Div> const&)::$_0&&, std::variant<Add, Sub, Mul, Div> const&)"
        mov     rax, qword ptr [rsi + 16]
        mov     rdx, qword ptr [rsi + 24]
        ret

std::__detail::__variant::__gen_vtable<true, std::basic_string_view<char, std::char_traits<char> >, getToken(std::variant<Add, Sub, Mul, Div> const&)::$_0&&, std::variant<Add, Sub, Mul, Div> const&>::_S_vtable:
        .quad   std::__detail::__variant::__gen_vtable_impl<true, std::__detail::__variant::_Multi_array<std::basic_string_view<char, std::char_traits<char> > (*)(getToken(std::variant<Add, Sub, Mul, Div> const&)::$_0&&, std::variant<Add, Sub, Mul, Div> const&)>, std::tuple<std::variant<Add, Sub, Mul, Div> const&>, std::integer_sequence<unsigned long, 0ul> >::__visit_invoke(getToken(std::variant<Add, Sub, Mul, Div> const&)::$_0&&, std::variant<Add, Sub, Mul, Div> const&)
        .quad   std::__detail::__variant::__gen_vtable_impl<true, std::__detail::__variant::_Multi_array<std::basic_string_view<char, std::char_traits<char> > (*)(getToken(std::variant<Add, Sub, Mul, Div> const&)::$_0&&, std::variant<Add, Sub, Mul, Div> const&)>, std::tuple<std::variant<Add, Sub, Mul, Div> const&>, std::integer_sequence<unsigned long, 1ul> >::__visit_invoke(getToken(std::variant<Add, Sub, Mul, Div> const&)::$_0&&, std::variant<Add, Sub, Mul, Div> const&)
        .quad   std::__detail::__variant::__gen_vtable_impl<true, std::__detail::__variant::_Multi_array<std::basic_string_view<char, std::char_traits<char> > (*)(getToken(std::variant<Add, Sub, Mul, Div> const&)::$_0&&, std::variant<Add, Sub, Mul, Div> const&)>, std::tuple<std::variant<Add, Sub, Mul, Div> const&>, std::integer_sequence<unsigned long, 2ul> >::__visit_invoke(getToken(std::variant<Add, Sub, Mul, Div> const&)::$_0&&, std::variant<Add, Sub, Mul, Div> const&)
        .quad   std::__detail::__variant::__gen_vtable_impl<true, std::__detail::__variant::_Multi_array<std::basic_string_view<char, std::char_traits<char> > (*)(getToken(std::variant<Add, Sub, Mul, Div> const&)::$_0&&, std::variant<Add, Sub, Mul, Div> const&)>, std::tuple<std::variant<Add, Sub, Mul, Div> const&>, std::integer_sequence<unsigned long, 3ul> >::__visit_invoke(getToken(std::variant<Add, Sub, Mul, Div> const&)::$_0&&, std::variant<Add, Sub, Mul, Div> const&)

Sadly, using the common base class does not lead to the compiler realizing that all the cases have identical code - you still keep the jump table: https://godbolt.org/z/GTyNgP

You could theoretically roll with a custom variant-like type erasure data structure that is aware of all the types having a common base class and going through that instead. But this moves in the direction of just having a NodeBase class with the common interface etc.

Max Langhof
  • 23,383
  • 5
  • 39
  • 72
  • Thanks for all the details. I feel better about the visitor approach knowing that it's going to use a jump table, which makes it no more expensive than calling a virtual method. – Willis Blackburn Dec 10 '19 at 17:03
2

Assume for a second that the base class offset would differ per class (there's no guarantee in general that base classes have zero offset in C++). Obviously that means you need to know the actual type stored in the variant to locate the base class subobject.

Once you realize this, you see why your options work:

  • If the common fields are part of Node, they're always at a fixed offset in Node
  • If you use a visitor, it finds the actual type and thus can compute a per-type offset to the base class subobject.
MSalters
  • 173,980
  • 10
  • 155
  • 350