5

The structured bindings feature says that it goes with the tuple like decomposition if the tuple_size template is a complete type. What happens when std::tuple_size is a complete type for the given type at one point in the program and is not complete at another point?

#include <iostream>
#include <tuple>

using std::cout;
using std::endl;

class Something {
public:
    template <std::size_t Index>
    auto get() {
        cout << "Using member get" << endl;
        return std::get<Index>(this->a);
    }

    std::tuple<int> a{1};
};

namespace {
    auto something = Something{};
}

void foo() {
    auto& [one] = something;
    std::get<0>(one)++;
    cout << std::get<0>(one) << endl;
}

namespace std {
template <>
class tuple_size<Something> : public std::integral_constant<std::size_t, 1> {};
template <>
class tuple_element<0, Something> {
public:
    using type = int;
};
}

int main() {
    foo();
    auto& [one] = something;
    cout << one << endl;
}

(Reproduced here https://wandbox.org/permlink/4xJUEpTAyUxrizyU)

In the above program the type Something is decomposed via the public data members at one point in the program and falls back to the tuple like decomposition at another. Are we violating ODR with the implicit "is std::tuple_size complete" check behind the scenes?

Curious
  • 20,870
  • 8
  • 61
  • 146
  • @DanielLangr which partial specialization are you referring to? – Curious Feb 08 '18 at 07:51
  • Why do you think the check "is `std::tuple_size` complete" will violate ODR? – xskxzr Feb 08 '18 at 07:59
  • @xskxzr An `is_complete` trait might result in two different instantiations from existing. One when the type is complete and another when the type is not complete, see https://stackoverflow.com/questions/1625105/for an example of such a trait – Curious Feb 08 '18 at 08:08

1 Answers1

5

I don't see any reason to believe that the program in question is ill-formed. Simply having something in the code depend on the completeness of a type, then having something else later on depend on the completeness of the same type where the type has since been completed, does not violate the standard.

A problem arises if we have something like

inline Something something;  // external linkage
inline void foo() {
    auto& [one] = something;
}

defined in multiple translation units, where, in some of those, std::tuple_size<Something> is already complete at the point where foo is defined, and in others, it isn't. This seems like it should definitely violate the ODR, since the entity one receives different types in different copies of foo, however, I can't actually find a place in the standard that says so. The criteria for the multiple definitions to be merged into one are:

  • each definition of D shall consist of the same sequence of tokens; and

  • in each definition of D, corresponding names, looked up according to 6.4, shall refer to an entity defined within the definition of D, or shall refer to the same entity, after overload resolution (16.3) and after matching of partial template specialization (17.8.3), except that a name can refer to

    • a non-volatile const object with internal or no linkage if the object

      • has the same literal type in all definitions of D,
      • is initialized with a constant expression (8.20),
      • is not odr-used in any definition of D, and
      • has the same value in all definitions of D,

      or

    • a reference with internal or no linkage initialized with a constant expression such that the reference refers to the same entity in all definitions of D;

    and

  • in each definition of D, corresponding entities shall have the same language linkage; and

  • in each definition of D, the overloaded operators referred to, the implicit calls to conversion functions, constructors, operator new functions and operator delete functions, shall refer to the same function, or to a function defined within the definition of D; and
  • in each definition of D, a default argument used by an (implicit or explicit) function call is treated as if its token sequence were present in the definition of D; that is, the default argument is subject to the requirements described in this paragraph (and, if the default argument has subexpressions with default arguments, this requirement applies recursively) 28 ; and
  • if D is a class with an implicitly-declared constructor (15.1), it is as if the constructor was implicitly defined in every translation unit where it is odr-used, and the implicit definition in every translation unit shall call the same constructor for a subobject of D.

If there's a rule here that makes my code ill-formed, I don't know which one it is. Perhaps the standard needs to be amended, because it cannot have been intended that this was allowed.

Another way to make the program ill-formed NDR involves the use of a template:

template <int unused>
void foo() {
    auto& [one] = something;
}
// define tuple_element and tuple_size
foo<42>(); // instantiate foo

This would run afoul of [temp.res]/8.4, according to which

The program is ill-formed, no diagnostic required, if ... the interpretation of [a construct that does not depend on a template parameter] in [the hypothetical instantiation of a template immediately following its definition] is different from the interpretation of the corresponding construct in any actual instantiation of the template

Brian Bi
  • 111,498
  • 10
  • 176
  • 312
  • How would the last one violate ODR? We are only instantiating the function once? And so we get the same definition that one time. – Curious Feb 08 '18 at 08:20
  • @Curious The actual instantiation is compared with the hypothetical instantiation that occurs immediately after the definition. – Brian Bi Feb 08 '18 at 08:28