Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can you violate ODR with structured bindings on a class type

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?

like image 295
Curious Avatar asked Feb 08 '18 07:02

Curious


People also ask

What is structured binding in a structured binding declaration?

A structured binding declaration performs the binding in one of three possible ways, depending on E. Case 1 : if E is an array type, then the names are bound to the array elements. Case 2 : if E is a non-union class type and tuple_size is a complete type, then the “tuple-like” binding protocol is used.

What is the difference between reference and structured binding?

Like a reference, a structured binding is an alias to an existing object. Unlike a reference, the type of a structured binding does not have to be a reference type. identifier-list : List of comma separated variable names.

Can you have a structured binding return a value?

You can have them return values, and the structured binding will simply capture values rather than references. This is handy if the underlying object doesn’t have access to references. We’ll see an application of this trick next time.

What is E in a structured binding declaration?

Otherwise e is defined as if by using its name instead of [ identifier-list ] in the declaration. We use E to denote the type of the expression e. (In other words, E is the equivalent of std::remove_reference_t<decltype((e))> .) A structured binding declaration then performs the binding in one of three possible ways, depending on E :


1 Answers

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

like image 183
Brian Bi Avatar answered Oct 16 '22 12:10

Brian Bi