Home > OS >  Virtual functions in class instantiated with std::tuple<void>
Virtual functions in class instantiated with std::tuple<void>

Time:02-10

I am working on a class that accepts a tuple of types of messages this obect will receive. In our model, we're using void to express the existence of a message that does not carry data, just a signal. The tuple is never actually instantiated but we inspect its types and generate various callbacks based on them. I have seen several references that suggest the use of std::tuple as replacement for variadic templates and the use of void in std::tuple (e.g., Void type in std::tuple).

All seems to work well, until I introduce a virtual member function that calls an external function:

#include <tuple>

void foo(void *);

template <typename TupleT>
class TT {
public:
    TT(){}

    void bar() {
    }

    void work() {
      bar();       // OK
      foo(this);   // UPDATE: NOT OK
    }

    virtual void fail() {
      bar();       // OK
      foo(this);   // NOT OK
    }
};

TT<std::tuple<void>> tt;

The compiler error I get tells me that the std::tuple<void> is actually instantiated in the virtual member function:

In file included from <source>:1:
/opt/compiler-explorer/gcc-11.2.0/include/c  /11.2.0/tuple: In instantiation of 'struct std::_Head_base<0, void, false>':
/opt/compiler-explorer/gcc-11.2.0/include/c  /11.2.0/tuple:407:12:   required from 'struct std::_Tuple_impl<0, void>'
/opt/compiler-explorer/gcc-11.2.0/include/c  /11.2.0/tuple:599:11:   required from 'class std::tuple<void>'
<source>:26:10:   required from 'void TT<TupleT>::fail() [with TupleT = std::tuple<void>]'
<source>:25:18:   required from here
/opt/compiler-explorer/gcc-11.2.0/include/c  /11.2.0/tuple:182:17: error: forming reference to void
  182 |       constexpr _Head_base(const _Head& __h)
      |                 ^~~~~~~~~~
/opt/compiler-explorer/gcc-11.2.0/include/c  /11.2.0/tuple:219:7: error: forming reference to void
  219 |       _M_head(_Head_base& __b) noexcept { return __b._M_head_impl; }
      |       ^~~~~~~
/opt/compiler-explorer/gcc-11.2.0/include/c  /11.2.0/tuple:222:7: error: forming reference to void
  222 |       _M_head(const _Head_base& __b) noexcept { return __b._M_head_impl; }
      |       ^~~~~~~
/opt/compiler-explorer/gcc-11.2.0/include/c  /11.2.0/tuple:224:13: error: 'std::_Head_base<_Idx, _Head, false>::_M_head_impl' has incomplete type
  224 |       _Head _M_head_impl;
      |             ^~~~~~~~~~~~
/opt/compiler-explorer/gcc-11.2.0/include/c  /11.2.0/tuple:224:13: error: invalid use of 'void'
/opt/compiler-explorer/gcc-11.2.0/include/c  /11.2.0/tuple: In instantiation of 'struct std::_Tuple_impl<0, void>':
/opt/compiler-explorer/gcc-11.2.0/include/c  /11.2.0/tuple:599:11:   required from 'class std::tuple<void>'
<source>:26:10:   required from 'void TT<TupleT>::fail() [with TupleT = std::tuple<void>]'
<source>:25:18:   required from here

Can someone explain to me why the compile attempts to instantiate the tuple in a virtual member function?

The obvious workaround seems to be to call a non-virtual member function and move the external call into it (calling works() from fail() in the example instead of calling foo() directly). While this works, it is rather unsatisfactory and I'd like to understand what happens here.

UPDATE: As pointed out in the comments, a call to work() causes the same problem (I thought I had tested it but compiled the wrong file...). I'd still like to understand why passing a pointer to TT has the compiler instantiate the tuple...?

Thanks for any hints :)

CodePudding user response:

See [temp.inst]/4:

Unless a member of a templated class is a declared specialization, the specialization of the member is implicitly instantiated when the specialization is referenced in a context that requires the member definition to exist ...

Thus, a member function of TT<std::tuple<void>> will only be instantiated once you do something that requires it to have a definition, and not before.

As long as you don't call TT<std::tuple<void>>::work, take its address, or otherwise odr-use it, it's not required to have a definition.

However, TT<std::tuple<void>>::fail is different because it is virtual. According to [basic.def.odr]/7, a virtual function is always considered to be odr-used (which forces its definition to exist) unless it is pure. That implies that as soon as the enclosing class is instantiated, which brings the virtual function into existence, the definition of that function must also be instantiated. Informally, the reason for this is that the implementation must generate the definition so that it can set up the vtable for the class.

So far, we've established that your program necessarily instantiates TT<std::tuple<void>>::fail. If you call work, that function is also instantiated. In either case, the expression foo(this) is then instantiated and a compilation error occurs. The remaining question is why foo(this) tries to instantiate std::tuple<void>?

The answer is that this is an unqualified function call and is subject to argument-dependent lookup. To perform this lookup, the compiler must determine the associated entities of decltype(this). This will "look through" pointers and recursively descend into type template arguments and base classes. So the compiler ends up determining that it must know the set of base classes of std::tuple<void> in order to complete the argument-dependent lookup. But the base classes of a class are specified as part of its definition, so the compiler has no choice but to instantiate std::tuple<void>.

You may suppress argument-dependent lookup by calling ::foo(this). In that case, your code will compile.

One possible way to avoid these issues entirely is to use std::tuple<std::type_identity<void>> instead (note the absence of _t). std::type_identity<T> is an empty struct for all T and is well-behaved as an element of a std::tuple.

CodePudding user response:

This is ADL causing trouble again: the std::tuple specialization is an associated entity via your template argument, and so it must be checked for friend declarations when (a pointer to) a TT is an argument.

  • Related