3

Consider this simple class derived from std::variant:

#include <string>
#include <variant>

class my_variant : public std::variant<int, std::string>
{
    using variant::variant;
public:
    std::string foo()
    {
        return "foo";
    }
};

Notice, I am deliberately adding using variant:variant so that constructors from the base class are exposed in the derived class. If you do not do this, instances of my_variant class will not work with operator= without a bunch of re-definitions. This compiles just fine.


Now let's start turning it into a template step-by-step.

template <typename TChr>
class my_variant : public std::variant<int, std::string>
{
    using variant::variant;

public:
    // Changed to templated return type, compiles fine
    std::basic_string<TChr> foo() 
    {
        return "foo";
    }
};

Here, the only change is that we make use of the template parameter in the foo() method. Everything still compiles fine.

And now this:

template <typename TChr>
class my_variant : // Changed to templated base class
    public std::variant<int, std::basic_string<TChr>> 
{
    // Now this line won't compile !!
    using variant::variant; 

public:
    std::basic_string<TChr> foo()
    {
        return "foo";
    }
};

As soon as I make use of the template parameter to describe the base class, I am getting the following compiler error:

'variant': is not a class or namespace name 

for this line: using variant::variant;

I do not fully understand why this specific change causes a problem. So far I was thinking in the direction that maybe using variant::variant without specifying its template signature is a problem, so I tried this:

template <typename TChr>
class my_variant :
    public std::variant<int, std::basic_string<TChr>>
{
    // Added templating here, still fails with the same error message.
    using variant<int, std::basic_string<TChr>>::variant; 

public:
    std::basic_string<TChr> foo()
    {
        return "";
    }
};

Doing so generates the same error message, so I must be doing something wrong.

Compiler is MSVC 2022, C++20 mode.

JeJo
  • 30,635
  • 6
  • 49
  • 88
Regus Pregus
  • 560
  • 3
  • 12
  • 3
    Deriving from std::variant is not really suppored it's [destructor](https://en.cppreference.com/w/cpp/utility/variant/~variant) is not virtual AND it has not virtual methods. Looks like you're better of aggregating a variant in you own class. Also look at this answe [inheriting from classes without virtual destructor (and methods)](https://stackoverflow.com/questions/9909439/inheriting-from-classes-without-virtual-destructors). Anyway I do not recommend using inheritance if the only reason is code reuse. – Pepijn Kramer Jul 04 '23 at 13:31
  • 2
    not saying that this is a moot question, but I'd argue that inheriting from `std::variant` isnt the best idea to begin with. I mean one main motivation for `std::variant` is as alternative to inheritance. In a nutshell, dont inherit, but visit! – 463035818_is_not_an_ai Jul 04 '23 at 13:36
  • 3
    @PepijnKramer deriving from non virtual classes works fine, so long as you don't treat it like Java. C++ inheritance interacts with template agument deduction in.ways that cannot be reproduced via other techniques – Yakk - Adam Nevraumont Jul 04 '23 at 13:44
  • 1
    @Yakk-AdamNevraumont I know it can work (give or take a few caveats). But the fact that it can work is not the same as recommended or good practice. (overuse of) Inheritance has a tendency to result in too tightly coupled software that is almost impossible to refactor. – Pepijn Kramer Jul 04 '23 at 15:21

2 Answers2

5

[...] So I must be doing something wrong!!

Since in the last my_variant, the parent std::variant depends upon the class template argument, you also need the constructor from the correct parent


template <typename TChr>
class my_variant : public std::variant<int, std::basic_string<TChr>> 
{
    using std::variant<int, std::basic_string<TChr>>::variant;
    //    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 
public:
    std::basic_string<TChr> foo()
    {
        return "foo";
    }
};

That has been mentioned, also have a read here, for more about your approach:

Is it okay to inherit implementation from STL containers, rather than delegate?

JeJo
  • 30,635
  • 6
  • 49
  • 88
  • 2
    Thank you, specifying std namespace in front of variant::variant indeed eliminated the problem. Curiously, if I do the same for the first and second examples (those that were compiling fine), they won't compile now. And yes, thank you for the link. – Regus Pregus Jul 04 '23 at 13:44
1

To expand on JeJo's answer:

The key idea here is name lookup. Whenever you use a name in C++, the compiler must decide what entity that name refers to. Sometimes it can just look for a declaration in the current scope (or one of its parents), e.g.

void foo() { return foo(); }
                    ^~~

There's no foo declared in the current scope, but there's the function named foo in the parent scope, so we take that.

Inside a class or class template, another possibility kicks in: the injected-class-name.

int Foo = 42;
struct Foo {
    void f(Foo&);
           ^~~

Here, the name Foo means "this class right here, Foo," even though there are multiple declarations of a name Foo in the parent scope. (C++ allows you to have a type and a variable with the same name at namespace scope, because C compatibility: think stat(3).) We call this the injected-class-name of Foo.

Injected-class-names also apply to base classes:

int Base = 42;
struct Base {};
struct Foo : Base {
    void f(Base&);
           ^~~~

There's no Base in the current scope, so we look in the parent scope, which is the base-class scope. There is a Base in that scope: the injected-class-name that means "this Base right here." So this code is also okay.

Inside a class template, the injected-class-name refers to "this instantiation of Foo right here." So:

template<class T>
struct Foo {
    void f(Foo&);
           ^~~

this Foo means the same thing as if it had said Foo<T>.

But remember, all of this is a subsystem of name lookup. The other relevant quirk of name lookup is that during phase 1 of two-phase lookup (that is, before template instantiation), name lookup never peeks into dependent base classes. (It can't, because they're dependent on template arguments, and we don't know those arguments yet.) So (Godbolt):

template<class T>
struct Foo : NDBase<int>, DBase<T> {
    void f(NDBase&, DBase&);
           ^~~~~~   ^~~~~

The reference to NDBase is OK: that's the injected-class-name of NDBase<int> inherited from NDBase<int>. The reference to DBase is bad: that's not the injected-class-name of DBase<T>, because DBase<T> is a dependent base, so Foo isn't going to look up anything in it.

We can tell name lookup that DBase is supposed to be looked up as a member by prefixing it with Foo:: (for most kinds of members, prefer this->). Of course then we also need to tell the parser that this dependent name is going to be a typename, not a variable/function/etc.:

template<class T>
struct Foo : NDBase<int>, DBase<T> {
    void f(NDBase&, typename Foo::DBase&); // OK
};

In your original example, you can get away without the typename prefix because using ... is already a typename-only context. In other words, you can do either:

using my_variant::variant::variant;
  // express that `variant` is a member, so look it up in
  // the dependent base and find it as an injected-class-name

using std::variant<int, std::basic_string<TChr>>::variant;
  // avoid injected-class-names entirely, and just name the
  // base type relative to the parent scope

But you can't just say

using variant::~~~~

because the compiler doesn't know what variant means in this scope. (Because it refuses to peek into the dependent base class in order to find that there's a matching injected-class-name visible in there.)

Quuxplusone
  • 23,928
  • 8
  • 94
  • 159