1

I have a type variant2 derived from std::variant. Now I want to be able to early capture potential assignment errors in case the assigned type doesn't match one of the variants types.

However, the way I came up with suffers from the issue that the typedef of the inner std::variant is incomplete until the closing braces, so I can't pass that through a concept. What alternatives do I have?

(Note: In production code the function assign is a lot more complicated, hence I want to be able to capture errors for the sake of user-friendliness early.)

Demo

#include <concepts>
#include <iostream>
#include <string>
#include <type_traits>
#include <variant>

template <typename T, typename Variant>
struct is_variant_type;

template <typename T, template <typename...> typename Var, typename... Args>
struct is_variant_type<T, Var<Args...>>
    : public std::disjunction<std::is_same<T, Args>...> {};

template <typename T, typename Variant>
concept variant_type = is_variant_type<T, Variant>::value;

template <typename T>
struct variant2 : public std::variant<std::monostate, int, T> 
{
    using inner_variant_type = variant2::variant;

    template <variant_type<inner_variant_type> U>
    auto assign(const U& arg) 
    {
        *this = arg;
    }
};

int main() 
{
    variant2<std::string> var;
    var.assign(2);
}

Yields:

<source>:19:42: error: invalid use of incomplete type 'struct variant2<T>'
   19 |     using inner_variant_type = variant2::variant;
      |                                          ^~~~~~~
<source>:18:8: note: definition of 'struct variant2<T>' is not complete until the closing brace
   18 | struct variant2 : public std::variant<std::monostate, int, T> {
      |        ^~~~~~~~
<source>:21:28: error: 'inner_variant_type' was not declared in this scope; did you mean 'is_variant_type'?
   21 |     template <variant_type<inner_variant_type> U>
      |                            ^~~~~~~~~~~~~~~~~~
      |                            is_variant_type

etc.

JeJo
  • 30,635
  • 6
  • 49
  • 88
glades
  • 3,778
  • 1
  • 12
  • 34
  • Have you considered using composition instead of inheritance? It feels like it would address many issues. – Fantastic Mr Fox Jun 13 '23 at 05:45
  • @FantasticMrFox I wanted to avoid forwarding the whole interface for std::variant. It worked quite well until now. – glades Jun 13 '23 at 06:09
  • `std::variant` doesn't have a virtual destructor, and therefore inheriting from it might cause problems further down the road (if somebody will use `variant2` in a polymorphic way). – wohlstad Jun 13 '23 at 06:20
  • @wohlstad The risk is the same as inheriting from std::variant, however I do not add nonscalar fields so nothing to be done in virtual cleanup, it should be fine. – glades Jun 13 '23 at 06:50
  • "I do not add nonscalar fields" It is still UB, regardless. – n. m. could be an AI Jun 13 '23 at 17:20
  • @n.m. How is this UB then? What could possibly go wrong? – glades Jun 14 '23 at 10:06
  • It is UB because the standard says so, not because "there is nothing there to go wrong". – n. m. could be an AI Jun 14 '23 at 10:27
  • @n.m. You're of course right as I saw. Leaves the question why the committee decided to keep it UB, the base object could just be destroyed via normal destructor and done... But yeah, I will put a word of caution. – glades Jun 15 '23 at 06:51

1 Answers1

4

You are missing a typename keyword, for the dependent type variant2::variant, in the alias declaration inside the class variant2.

Additionally, you have to mention which operator=, that you meant at the line *this = var;, which is by default not visible to the child. You can have more read here: Inheritance and templates in C++ - why are inherited members invisible?

template <typename T>
struct variant2 : public std::variant<std::monostate, int, T> 
{
    using inner_variant_type = typename variant2::variant;
    //                         ^^^^^^^^ ---> required 

    // required to mention, that the operator= is from the parent !!
    using std::variant<std::monostate, int, T>::operator=;  

    template <variant_type<inner_variant_type> U>
    auto assign(const U& var) {
        *this = var;
    }
};

See a demo

JeJo
  • 30,635
  • 6
  • 49
  • 88
  • That does it already! I'm pretty delighted that I don't even have to work around this, thanks! Btw you could also enter the dependent name to declare operator= like so: `using variant2::variant::operator=`; which is tad less verbose. – glades Jun 13 '23 at 06:13
  • @glades Yes of, course. However, consider packing the `std::variant` inside of your `variant2`, rather than inheriting. See [Is it okay to inherit implementation from STL containers, rather than delegate?](https://stackoverflow.com/questions/2034916/is-it-okay-to-inherit-implementation-from-stl-containers-rather-than-delegate). This is also applied for `std::variant`. – JeJo Jun 13 '23 at 06:24
  • I have considered that, but I have no additional nonscalar fields inside my class so nothing to be done through a virtual destructor :) Also the class uses things like recursive nesting and idk if it can be done so easily via composition. – glades Jun 13 '23 at 06:49