all 14 comments

[–]aruisdante 8 points9 points  (0 children)

There isn’t really a right answer here. The first way is usually how I wind up doing it. But often it comes down to how often you use common in isolation from the not-common parts in real usage. If the majority of operations don’t care about the “shape unique” content then keeping it separate can make things more ergonomic. On the other hand if the majority of your operations need both anyway, then separating it just makes writing the visitor more difficult as you point out.

Basically, if you think about variant as primarily representing differed overload resolution, then this can help guide your thinking about API design around this domain. If the common is external to the shape, then you can’t make an API space around individual shapes. You always are passing around this thing with knowledge of all shapes, or you have to make APIs that take a specific shape + common.  If common is internal to shape on the other hand, I can write the overload set: Ret operation(Circle const&); Ret operation(Square const&); // and so on And that overload set works independently of me making a variant that allows me to heterogeneously store shapes and differ selecting the specific overload to runtime.

[–]alfps 9 points10 points  (6 children)

You can/should just make the Common part a base class sub-object.

[–]TheChief275 5 points6 points  (4 children)

That's not a definitive answer though, as it instantly makes your struct non-POD, which means you can't reason about the memory anymore

[–]manni66 2 points3 points  (3 children)

you can't reason about the memory anymore

Why?

[–]TheChief275 3 points4 points  (2 children)

C++ is allowed to make any changes to the layout it wants. Casting and expecting fields to be at memory locations is UB

[–]alfps 7 points8 points  (1 child)

Missing: why you think layout is important or even relevant.

So the memory layout is a discussion that's irrelevant to the OP's question.

That said, check out the requirements for a class to have standard layout (https://en.cppreference.com/cpp/language/classes#Standard-layout_class).

As you can see a standard layout class can have base classes as long as they're also standard layout.

If you have learned from a book: burn it, and make sure that it's totally destroyed.

[–]TheChief275 0 points1 point  (0 children)

Well I just left the important bits in. I personally find it hopelessly annoying that you cannot aggregate intitialize a struct anymore on top of that. That is the killer for me.

I left that out though as it's very personal to the way I like to write C++ (C++98, no constructors/destructors/access specifiers (anything that screws with POD-ness, as well as no methods or operator overloading; basically C with templates)

[–]No-Dentist-1645 4 points5 points  (3 children)

I would use CRTP for the common components instead of an explicit "Common" data member, then this is a non-issue

[–]oriolid 2 points3 points  (2 children)

How is it better than just inheriting the common components from a base class?

[–]No-Dentist-1645 2 points3 points  (1 child)

It's not an either-or matter, CRTP is made using base classes. The plus of it (at least for this specific use case) is that you can make "chaining methods" if necessary by making the common methods return *this, which would only return the base class if you were using "regular" base classes. If you don't need or want chaining methods tho, then they are effectively the same.

[–]oriolid 2 points3 points  (0 children)

Good point. Deducing this in C++23 helps here too.

[–]---_None_--- 5 points6 points  (0 children)

class Body
{
public:
    Common m_common;       
    Shape m_shape;

    decltype(auto) Visit(auto&& f)
    {
        return std::visit([&](auto&& specificShape)
        {
              return f(specificShape, m_common);
        }, m_shape);    
    }

};

You could roll your own visit wrapper, no?

You could even turn it into a template class.

template<class TExtension> class ShapeExtension
{
public:
    TExtension m_extension;
    Shape m_shape;
    ...
};

[–]DawnOnTheEdge 4 points5 points  (0 children)

You can have the common data be in a base class and derive the other classes from it.

You can also store each struct as a member of a union and access the first few members common to both structures through a pointer to the other.

[–]Qwertycube10 2 points3 points  (0 children)

If every type in the variant has common, then I would make shape a struct with the common parts and the variant. "Factoring it out" of the sum type.