Stl: <tuple>: correct `get` couldn't be found for struct inheriting from tuple

Created on 20 Sep 2019  路  9Comments  路  Source: microsoft/STL

Hi,
It was found that the code below doesn't compile on Windows with clang++. Here we create a class inherits tuple and try to call get for it.

I use Visual Studio 2019. Version 16.2.3. Compiler clang++

#include <iostream>
#include <tuple>

template <typename... _Tp>
struct TupleWrapper : public std::tuple<_Tp...>
{
    typedef std::tuple<_Tp...> base_type;

    TupleWrapper(const base_type& t) : base_type(t) {}

#if 0 // 1
    template <size_t i>
    friend const auto&
        get(const TupleWrapper& t)
    {
        return std::get<i>(base_type{ t });
    }
#endif
};

int main()
{
    TupleWrapper<int, int> a = std::make_tuple(1, 2);
    using std::get;
    std::cout << get<0>(a) << " " << get<1>(a) << std::endl;
    return 0;
}
>clang++ -std=c++14 -O0 test.cpp -otest.exe
test.cpp:26:38: error: no matching function for call to 'get'
    std::cout << get<0>(a) << " " << get<1>(a) << std::endl;
                                     ^~~~~~
C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\VC\Tools\MSVC\14.22.27905\include\utility:452:63: note: candidate template ignored: could not match 'pair' against 'TupleWrapper'
_NODISCARD constexpr tuple_element_t<_Idx, pair<_Ty1, _Ty2>>& get(
                                                              ^
.........................................
C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\VC\Tools\MSVC\14.22.27905\include\tuple:638:65: note: candidate template ignored: failed template argument deduction
_NODISCARD constexpr tuple_element_t<_Index, tuple<_Types...>>& get(
                                                                ^
C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\VC\Tools\MSVC\14.22.27905\include\tuple:645:71: note: candidate template ignored: failed template argument deduction
_NODISCARD constexpr const tuple_element_t<_Index, tuple<_Types...>>& get(
                                                                      ^
C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\VC\Tools\MSVC\14.22.27905\include\tuple:652:66: note: candidate template ignored: failed template argument deduction
_NODISCARD constexpr tuple_element_t<_Index, tuple<_Types...>>&& get(

Could you please help to identify what is the root case of the problem?

bug

All 9 comments

Aside: Your program formally has undefined behavior due to use of the reserved identifier _Tp per [lex.name]/3.1,

Could you please help to identify what is the root case of the problem?

Our tuple implementation is recursive; tuple<int, double, void*> derives from tuple<double, void*> derives from tuple<void*> derives from tuple<> (https://godbolt.org/z/Er2tqy). This is unfortunate for a few reasons, including the one you've stumbled across.

When you pass an instance of a specialization of tuple to std::get:

static_assert(std::get<0>(tuple<int, double, void*>{42, 3.14, nullptr}));

the compiler must deduce tuple<Types...> from e.g. tuple<int, double, void*>. The deduction is simple when the source is a specialization of the same template like this: Types... is deduced to be int, double, void*, and all is well (https://godbolt.org/z/pFtzMl).

If we instead derive a type from a specialization of tuple:

struct S : std::tuple<int, double, void*> {};

and attempt to pass an object of _that_ type to std::get:

static_assert(std::get<0>(S{{42, 3.14, nullptr}}) == 42);

the compiler must deduce tuple<Types...> from S. Since S isn't a specialization of tuple, it looks for specializations of tuple from which S is publicly and unambiguously derived from which it can deduce Types. There are four such specializations in S's inheritance hierarchy:

  1. S's direct base std::tuple<int, double, void*>,
  2. std::tuple<int, double, void*>'s direct base std::tuple<double, void*> (an indirect base of S),
  3. std::tuple<double, void*>'s direct base std::tuple<void*> (an indirect base of S),
  4. std::tuple<void*>'s direct base std::tuple<> (an indirect base of S).

Now things get messy. The C++ Standard provides no rule to disambiguate which of these four different bases should be chosen, so a conforming compiler must throw its hands in the air, say the deduction fails due to ambiguity, and that it can't call std::get with an S (as does clang in your example). MSVC has an overload resolution bug which causes it to prefer direct bases in such cases so the deduction succeeds and deduces Types as the pack int, double, void*. We can't fix the compiler bug without breaking tons of MSVC-specific code with types that derive from tuple, and we can't change the design of tuple to avoid the unfortunate recursion in the first place without breaking ABI and hence even more code.

@CaseyCarter, thanks for the explanation. What is the advise in this case?
Can the wrapper be improved anyhow to use std::get for it?

We can't fix the compiler bug without breaking tons of MSVC-specific code with types that derive from tuple

/permissive- could guard such enforcement.

and we can't change the design of tuple to avoid the unfortunate recursion in the first place without breaking ABI and hence even more code.

I have a prototype indicating that we can flatten the inheritance while preserving layout, so it may be possible.

@CaseyCarter, thanks for the explanation. What is the advise in this case?
Can the wrapper be improved anyhow to use std::get for it?

I don't know of a way to make the wrapper work with std::get. The best advice I have is to write get overloads for the wrapper, and either perform the ADL two-step:

using std::get; 
get<X>(e);

at callsites that need to handle both std::tuple and wrappers, or write a function to do the ADL two-step for you:

template<std::size_t I, class T>
constexpr decltype(auto) adl_get(T&& t) noexcept {
  using std::get;
  return get<I>(std::forward<T>(t));
}

and use that at your callsites.

(Caveat: pair and tuple are to be avoided whenever possible. Prefer types with properly-named members. Reuse via inheritance is also to be avoided when you can reuse via composition.)

Now things get messy. The C++ Standard provides no rule to disambiguate which of these four different bases should be chosen, so a conforming compiler must throw its hands in the air, say the deduction fails due to ambiguity, and that it can't call std::get with an S (as does clang in your example).

This is outdated; CWG fixed it in Kona via CWG2303.

I have a prototype indicating that we can flatten the inheritance while preserving layout, so it may be possible.

@StephanTLavavej, is it implemented in STL now?

To clarify for myself: Are you saying that the error appears, even, if

template <size_t i>
friend const auto&
    get(const TupleWrapper& t)
{
    return std::get<i>(base_type{ t });
}

is defined or only without it? In your example, that definition is disabled through the #if 0.

The error appears if that function is not defined

@capatober No, I didn't implement that tuple representation overhaul, and I now believe that attempting to do so near the end of the v19 ABI releases would be unnecessarily disruptive. I think we should just fix this in vNext even if we could technically fix it in v19.

Was this page helpful?
0 / 5 - 0 ratings