Adding support for data member packs

Problem

Implementing product and sum types such as std::tuple and std::variant currently rely on lots of template metaprogramming. For example, libc++ roughly implements std::tuple as follows:

template<size_t I, class T>
struct __tuple_leaf {
  static constexpr size_t value = I;
  T t;
};

template<class... Ts>
struct tuple : __tuple_leaf<sizeof(Ts), Ts>...
{};

It’s substantially more complex (and tuple doesn’t directly inherint from __tuple_leaf), but the idea is that std::tuple picks up Ts... without needing to recursively instantiate anything. This is an improvement over recursively inheriting std::tuple. After turning libc++’s std::tuple into an aggregate and removing anything else that might be unrelated to a comparison with a hand-written struct that declares all of its elements, it seems that the libc++ implementation is 2.88x the size of something hand-written in debug mode. It’s interestingly smaller when debug symbols are disabled and optimisations are enabled, but this could be an artifact of me not doing a good job of preventing the optimiser from pruning unused stuff (@dblaikie has some ideas on why this might be happening).

This doesn’t take into consideration things like std::get, std::tuple_cat, std::visit, etc., all of which will create lots more in the way of program size.

Proposed solution

It would be good for there to be a way to type out

template<class... Ts>
struct tuple {
  Ts... ts;
};

which would be structurally equivalent to

template<class T0, class T1, /* ... */, class Tn>
struct tuple {
  T0 t0;
  T1 t1;
  // ...
  Tn tn
};

Element access

Member element access happens as described in P1858 and P2662. That is, we use the subscript operator (ts...[0] to access t0), and so on. To access the n th element of ts, there must be n + 1 elements in the parameter pack; otherwise it’s a compile-time error.

Construction

When constructing a data member pack, it would look similar to how we inherit from a parameter pack.

template<class... Ts>
class tuple {
public:
  tuple(Ts&... ts)
  : ts_(ts)...
  {}

  template<class... Us>
  requires (sizeof...(Us) == sizeof...(Ts)) and 
           (std::constructible_from<Ts, Us> and ...)
  tuple(Us&&... us)
  : ts_(std::forward<Us>(us))...
  {}
private:
  Ts... ts_;
};

Benefits

  • improved program sizes at both compile-time and runtime;
  • improved compilation speeds since we don’t need to instantiate so many types to facilitate std::tuple and std::variant (and third-party equivalents);
  • more readable diagnostics, since names will be smaller;
  • potentially improved debugger performance for very large programs;
  • substantially simplified implementations such as
template<size_t N, class... Ts>
T& get(tuple<Ts...>& t) noexcept
{
  return t.ts...[N];
}

Costs

  • either needs to be a Clang extension or standardised in C++26
  • standard libraries might not be able to migrate without breaking ABI
  • @zygoloid notes that this introduces the possibility of encountering parameter packs outside of templates, which Clang doesn’t have any way of modelling at the moment. Unless it would require a complete redesign of the feature, I think we could implement this bit independently of the template section, although it absolutely needs to be implemented. I don’t fully understand the implementation concerns on this point, and I think we need to discuss this in greater detail.

Extension or standardisation?

I intend to write a proposal to WG21 to standardise this feature (libc++ cannot be expected to maintain two versions of std::tuple and std::variant for two different compilers). However, this feature is already on the committee’s radar thanks to P1858, and I consider it critically important for there to be an implementation before features are standardised; preferably with deployment experience so that we can see the benefits and drawbacks of the feature in practice, as opposed to trying to work them out from reasoned motivation. As such, the committee proposal for this feature will happen in parallel to its implementation.

The first revision of the proposal will be largely similar to this proposal, likely with changes applied as feedback is received.

In terms of actual assembly code in your example, the “cost” is an artifact of a naive CodeGen strategy for aggregate initialization from constants. When a variable is initialized, we have a heuristic to choose whether to emit inline code or memcpy from a global, and that heuristic apparently isn’t working correctly for std::tuple. If someone wants to look at it, it’s probably easy to fix. If you replace the constants with variables, you’ll find the unoptimized assembly is exactly the same.

The cost of the additional template instantiations seems unlikely to be noticeable in terms of compile-time. The cost of the additional debug info is real, I guess, if you use tuples heavily.

I don’t think this is worth it for the sake of the standard library; we can’t use it for std::tuple itself (for ABI reasons, and because it doesn’t allow std::tuple’s empty class optimization). And even if we could use it, the benefits are fringe at best.

I won’t try to debate the benefits of this for people writing their own libraries. (I can’t think of any context where I’d want to use it, but I don’t do very much of the sort of template metaprogramming where it would be relevant.)

Ah, yeah, P1858 looks like it covers this feature, so I’m not sure it’s necessary to write another proposal? What’s the state of P1858? (is it still ongoing/seems to be OK?)

Seems fine to me to say you’re implementing a prototype of part of this proposal.

1 Like

it doesn’t allow std::tuple’s empty class optimization

Which EBCO are you referring to here? The libc++ implementation for std::tuple doesn’t inherit from anything.

Further, I don’t see why a member pack would fail to offer EBCO when the pack is empty (or has [[no_unique_address]] on it.

The cost of the additional debug info is real, I guess, if you use tuples heavily.

Tuples may or may not be the primary beneficiary of this feature (though this may be impacted with std::ranges::zip). std::variant is the type I think will enjoy the most benefit from this: I just find it easier to talk about std::tuple due to it being a simpler type.

__tuple_impl inherits from __tuple_leaf, which inherits from empty types.

I guess marking the pack no_unique_address has roughly the right effect, sure, assuming that has the obvious semantics.

Oh, wow, std;:variant is really ugly. I can see why you’d want it there.

That said, if someone cared, it’s probably feasible to simplify the IR generated at -O0 using “if consteval”. A lot of the implementation complexity centers around avoiding reinterpret_cast.

I got the feeling that P1858 is an “ideas” paper moreso than a “we should consider the technical nuances of this specific feature” paper. Daisy Hollman pointed out some technical problems if this is naively done, so a dedicated paper may be worthwhile.

fair enough - can’t say I’m all up on the nuances of how these things work on the committee

Hey Christopher.

My plan was to implement this in clang over the next 6 months (not clear i would do that before or after universal template parameters but it’s in the pipeline, targeting either Kona or Tokyo, I should have a better timeline soon.
We should synchronize as multiple proposals aren’t likely to be productive in WG21.

I think we all mostly agree that this is an important problem to solve, but I would be very uneasy (ie, i think this is a terrible idea) with making it a clang extension as there are many syntax and design elements that need to be considered, most of which are explored but not necessarily resolved by P858.

  • We need a syntax construct to distinguish packs and non types nested in a dependent types (equivalent to typename and template, in a way that combines with those
  • We need to figure what to do when there are packs nested within packs, which can happen with member packs
  • We need, and this is the more complicated part, to find syntaxes that are agreeable to other implementations, one of which communicated packs in non templates requires heroics on their part. They may require some syntax announcing the presence of packs (same as there is for pack in structured bindings), so an implementation is not actually that helpful in increasing consensus, even if i think it will be needed to really understanding nested packs and their expansions, and findinding all the wording that needs changing. Pack aliases are fairly similar and should certainly be part of the same effort.

I expect pack indexing and pack in structured bindings to progress in Kona (both papers are currently in CWG but with a few interesting design questions left. Core also expressed concerned about the presence of TU/namespace-scopes packs in structured binding as they observed it makes the entire TU a template of sort, which is an interesting observation. Member packs would have the same properties and should have the same resolution.

Some of these things are also covered in https://wg21.link/p2632

That does not cover tuple<S, S> as [[no_unique_address]] only applies to different types, which means that either we are back missing with index sequences so that we can have a

struct tuple {
   [[no_unique_address]] unique_type<__pack_index_of(T), T>...;
};

or

struct tuple {
   [[really_no_unique_address]] T...;
};

And that needs solving one way or another as otherwise we would run into ABI issues.