Skip to content

Latest commit

 

History

History

tuple

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Circle tuple

Browse implementation tuple.hxx.

This is a Circle implementation of C++20's std::tuple class.

Also note the Circle variant and Circle mdspan implementations.

Contents.

First-class tuple support.

As documented here, the operators .[I] and [begin:end:step] subscript and slice "structured types." What types are these?

  • Tuple-like types that specialize std::tuple_size yield their elements.
    • std::tuple
    • std::pair
    • std::array
    • circle::tuple
  • Builtin arrays yield their elements.
  • Other classes and structs yield their non-static public data members.

The sizeof. operator returns the number of elements in a structured type. For a tuple-like type, this returns std::tuple_size. For an array, it returns the number of elements in the first rank. For other class types, it returns the number of non-static data members.

The tuple_elements member trait probes std::tuple_elements and yields the contained types as a type parameter pack. This is fully imperative, so you don't have to call a function that exposes template parameters that can be deduced to use it.

access.cxx - Compiler Explorer

#include <tuple>
#include <array>
#include <iostream>

int main() {
  std::tuple<int, double, const char*> tup(
    100, 3.14, "Hello std::tuple"
  );

  std::cout<< int...<<": "<< decltype(tup).tuple_elements.string<< "\n" ...;

  // Print out by subscript.
  std::cout<< "Print by subscript:\n";
  std::cout<< "  0: "<< tup.[0]<< "\n";
  std::cout<< "  1: "<< tup.[1]<< "\n";
  std::cout<< "  2: "<< tup.[2]<< "\n";

  // Print out by slice.
  std::cout<< "Print by slice - "<< sizeof. tup<< " elements:\n";
  std::cout<< "  "<< int...<< ": "<< tup.[:]<< "\n" ...;

  std::pair<const char*, long> pair(
    "A pair's string",
    42
  );
  std::cout<< "Works with pairs - "<< sizeof. pair<< " elements:\n";
  std::cout<< "  "<< int...<< ": "<< pair.[:]<< "\n" ...;

  int primes[] { 2, 3, 5, 7, 11 };
  std::cout<< "Works with builtin arrays - "<< sizeof. primes<< " elements:\n";
  std::cout<< "  "<< int...<< ": "<< primes.[:]<< "\n" ...;
}
$ circle access.cxx && ./access
0: int
1: double
2: const char*
Print by subscript:
  0: 100
  1: 3.14
  2: Hello std::tuple
Print by slice - 3 elements:
  0: 100
  1: 3.14
  2: Hello std::tuple
Works with pairs - 2 elements:
  0: A pair's string
  1: 42
Works with builtin arrays - 5 elements:
  0: 2
  1: 3
  2: 5
  3: 7
  4: 11

This sample shows the tuple subscript and slice operators on a variety of types. Because Circle imperatively will create a pack from a tuple's elements, you don't need indirection through an apply-like function for the purpose of argument deduction. Write your operation inline, and use a pack expansion at the end of the statement to transform each element.

Data member packs.

Member pack declarations are used in the Circle mdspan for partially-static storage and inside unions in the Circle variant.

pack1.cxx

#include <iostream>
#include <utility>

template<typename... Ts>
struct tuple {
  // Declare a parameter pack of data members with ...<name> declarator-id.
  [[no_unique_address]] Ts ...m;

  // Declare default, copy and move constructors.
  tuple() : m()... { }
  tuple(const tuple&) = default;
  tuple(tuple&&) = default;

  // Converting constructor. Note the ... after the m pack subobject init.
  template<typename... Ts2>
  requires((... && std::is_constructible_v<Ts, Ts2&&>))
  tuple(Ts2&&... x) : m(x)... { }

  // Specialize a single element. Subobject-initialize that one element,
  // and default construct the rest of them.
  template<size_t I, typename T>
  tuple(std::in_place_index_t<I>, T&& x) :
    m...[I](x), m()... { }

  // Use pack subscript ...[I] to access pack data members.
  template<int I>
  Ts...[I]& get() {
    return m...[I];
  }

  template<typename T>
  T& get() {
    // The requested type must appear exactly once in the tuple.
    static_assert(1 == (... + (T == Ts)));
    constexpr size_t I = T == Ts ...?? int... : -1;
    return m...[I];
  }
};

struct empty1_t { };
struct empty2_t { };
struct empty3_t { };

// Members of the same type do not alias under no_unique_address rules.
static_assert(3 == sizeof(tuple<empty1_t, empty1_t, empty1_t>));

// Members of different types do alias under no_unique_address rules.
static_assert(1 == sizeof(tuple<empty1_t, empty2_t, empty3_t>));

int main() {
  // Use the converting constructor to create a tuple.
  tuple<int, double, const char*> x(10, 3.14, "Hello tuple");
  std::cout<< x.get<0>()<< " "<< x.get<1>()<< " "<< x.get<2>()<< "\n";

  // Initialize only member 1 with 100.0.
  tuple<int, double, float> y(std::in_place_index<1>, 100);
  std::cout<< y.get<0>()<< " "<< y.get<1>()<< " "<< y.get<2>()<< "\n";

  // Print the int element of x and the double element of y.
  std::cout<< x.get<int>()<< " "<< y.get<double>()<< "\n";
}

The heart of the tuple implementation is the data member pack declaration Ts ...m. At definition, m is a parameter pack declaration, and any expression naming it is a pack expression. At instantiation, concrete data members are created, with names m0, m1, m2 and so on. These names are necessary for reflection and useful for error reporting.

The member pack is given the [[no_unique_address]], which helps compress the data structure by allowing empty members with different types alias to the same offset within the class. This is equivalent to the empty base optimization, but in a more useful form. It is prohibited to directly inherit from multiple base classes of the same type, but it is perfectly fine to expand a member pack with multiple elements of the same type. This member pack-driven tuple does away with the indexing wrappers that appear in every ISO C++ tuple implementation.

To initialize member packs subobjects in bulk, use m(args)..., the same syntax that you'd use to initialize base class pack subobjects. However, Circle also lets you initialize specific data members with the pack subscript operator m...[I](args). Initialize as many subscripted elements as you'd like, and then initialize the remainder of the pack with the bulk initializer m(args)....

Mapping types to indices.

Pack subscript makes implementing getter functions trivial. Take the element index as a template parameter and use ...[I] on the data member. std::tuple and std::variant also support getters on type template parameters, if that type parameter occurs exactly once in the container.

  template<typename T>
  T& get() {
    // The requested type must appear exactly once in the tuple.
    static_assert(1 == (... + (T == Ts)));
    constexpr size_t I = T == Ts ...?? int... : -1;
    return m...[I];
  }

We'll use an additive fold-expression to enforce the "occurs once" mandate. The operand of the fold is simply T == Ts, which compares each type in Ts to the get parameter T. If they're the same, the subexpression is true, which gets promoted 1, and summed up.

To map this unique type T to its index within Ts, use constexpr multi-conditional operator ??:. The left-hand operand is a pack expression predicate for the conditional. Substitution progress until it finds a pack element that evaluates true, and then it substitutes the corresponding pack element of the center operand, which it yields as the result of the expression. Our center operand is the pack index operator int.... This operator is itself a parameter pack expression, which yields the current index of expansion during substitution. Therefore, if T == Ts was true on pack element 3, int... yields 3 when substituted, which becomes the value of the index used for subscripting the data member pack m.

Deduced forward references.

The big invention added for implementing std::tuple is the deduced forward reference. This extension to overload resolution eliminates the need for multiple function overloads that only differ on the const, volatile or reference qualifiers of their parameters. Both the forwarding type of the function parameter and constituent parts of its type-id are deduced.

parameter-declaration : type-id

The syntax is simple: write a forwarding reference parameter declaration, like T&& u where T is a template parameter of that function, a colon, and a type-id for the type of the function parameter itself. Ordinary forwarding references are unconstrained, in that T can be deduced to be any type. But deduced forwarding parameters are set by the right-hand type, and function arguments must be converted to that. Effectively, the const, volatile and reference qualifiers are deduced from the argument, and applied to the right-hand type.

This is still, at heart, a forwarding reference. During overload resolution, type type of an lvalue argument is replaced by its own lvalue reference, and deduction takes place from there.

As an example:

template<typename T, typename... Args>
void f(T&& u : std::tuple<Args...>);

T can deduced to any of eight possible types, allowing this one function to take the place of up to eight separate overloads:

template<typename... Args>
void f(std::tuple<Args...>& u);

template<typename... Args>
void f(const std::tuple<Args...>& u);

template<typename... Args>
void f(volatile std::tuple<Args...>& u);

template<typename... Args>
void f(const volatile std::tuple<Args...>& u);

template<typename... Args>
void f(std::tuple<Args...>&& u);

template<typename... Args>
void f(const std::tuple<Args...>&& u);

template<typename... Args>
void f(volatile std::tuple<Args...>&& u);

template<typename... Args>
void f(const volatile std::tuple<Args...>&& u);

This sample uses Circle reflection to confirm that deduced forward references do properly handle const and non-const lvalue and xvalue argument types.

deduce.cxx

#include <iostream>
#include <utility>

template<typename... Ts>
struct tuple {
  Ts ...m;
};

template<typename T1>
void f1(T1&& x) {
  std::cout<< "  f1: "<< T1.string<< "\n";
} 

template<typename T2, typename... Args>
void f2(T2&& y : tuple<Args...>) {
  std::cout<< "  f2: "<< T2.string<<" | Args: ";
  std::cout<< Args.string<< " "...;
  std::cout<< "\n";
}

struct derived_t : tuple<int, char, void*> { };

int main() {
  derived_t d1;
  const derived_t d2;

  std::cout<< "lvalue:\n";
  f1(d1);
  f2(d1);

  std::cout<< "const lvalue:\n";
  f1(d2);
  f2(d2);

  std::cout<< "xvalue:\n";
  f1(std::move(d1));
  f2(std::move(d1));

  std::cout<< "const xvalue:\n";
  f1(std::move(d2));
  f2(std::move(d2));
}
$ circle deduce.cxx && ./deduce
lvalue:
  f1: derived_t&
  f2: tuple<int, char, void*>& | Args: int char void* 
const lvalue:
  f1: const derived_t&
  f2: const tuple<int, char, void*>& | Args: int char void* 
xvalue:
  f1: derived_t
  f2: tuple<int, char, void*> | Args: int char void* 
const xvalue:
  f1: const derived_t
  f2: const tuple<int, char, void*> | Args: int char void* 

P2481R0 - Forwarding reference to specific type/template highlights a number of problems with general unconstrained forwarding references. P0847R7 - Deducing 'this' specifically considers the "shadowing problem" in which deducing a derived type in an explicit 'this' function can make members of the base class inaccessible.

self.cxx

#include <iostream>
#include <utility>

struct B1 {
  template<typename Self>
  auto&& get0(this Self&& self) {
    // Error: ambiguous declarations found from id-expression 'i'.
    return std::forward<Self>(self).i;
  }

  template<typename Self>
  auto&& get1(this Self&& self) {
    // P00847R7: mitigate against shadowing by copy-cvref.
    return ((__copy_cvref(Self, B1)&&)self).i;
  }

  template<typename Self>
  auto&& get2(this Self&& self : B1) {
    // Circle deduced forward reference uses a normal forward.
    return std::forward<Self>(self).i;
  }

  int i;
};

struct B2 {
  int i;
};

struct D : B1, B2 { };

int main() {
  D d;

  // Uncomment this for ambiguous declaration error.
  // int x0 = d.get0();

  // Works with explicit upcast to B1.
  int x1 = d.get1();

  // Works with deduced forward reference.
  int x2 = d.get2();
}

The basic problem is that forwarding references, including those deducing "this" forwarding references, bind to the type of the argument passed, not to the type of the argument of interest. For general robustness, the user must upcast back to the argument type it wants, but in a way that preserves the const, volatile and reference qualifiers of the forwarding reference. The Circle method, as in get2, deduces the template parameter to a type B1, so that it can be used directly as the std::forward template argument.

tuple.cnstr

template<class U1, class U2> constexpr explicit(see below) tuple(pair<U1, U2>& u);
template<class U1, class U2> constexpr explicit(see below) tuple(const pair<U1, U2>& u);
template<class U1, class U2> constexpr explicit(see below) tuple(pair<U1, U2>&& u);
template<class U1, class U2> constexpr explicit(see below) tuple(const pair<U1, U2>&& u);

The C++ Standard defines std::tuple constructors and operators in sets of four: const and non-const, lvalue and rvalue parameter-taking. The deduced forwarding reference lets us write one overload and get functionality for all four:

tuple.hxx

  // Conversion constructor from std::pair.
  template<class T, class U1, class U2>
  requires(
    sizeof...(Types) == 2 &&
    std::is_constructible_v<Types...[0], __copy_cvref(T&&, U1)> &&
    std::is_constructible_v<Types...[1], __copy_cvref(T&&, U2)>
  )
  constexpr explicit(
    !std::is_convertible_v<__copy_cvref(T&&, U1), Types...[0]> || 
    !std::is_convertible_v<__copy_cvref(T&&, U2), Types...[1]>
  )
  tuple(T&& u : std::pair<U1, U2>) :
    m((get<int...>(std::forward<T>(u))))... { }

T is the forwarding reference of the pair parameter. It can assume on of these four types, to match each of the four overloads for the tuple constructor:

  1. std::pair&
  2. const std::pair&
  3. std::pair
  4. const std::pair

The constraint and explicit-specifier require determining the type of the pair element members as if they were accessed with a get around a forward. We can accomplish this with the new Circle compiler builtin __copy_cvref, which copies the const, volatile and reference qualifiers from the first operand to the type of the second operand.

Tuple cat.

The most difficult std::tuple function to implement using ordinary C++ is be tuple_cat:

template<class... Tuples>
constexpr tuple<CTypes...> tuple_cat(Tuples&&... tpls);

The libstdc++ implementation uses recursive partial template specialization. But this should be an easy operation. It's really just a double for loop: the outer loop visits the parameters in tpls, and the inner loop visits the tuple elements in each parameter.

tuple.hxx

template<class... Tuples>
constexpr tuple<
  for typename Ti : Tuples => 
    Ti.remove_reference.tuple_elements...
>
tuple_cat(Tuples&&... tpls) {
  return { 
    for i, typename Ti : Tuples =>
      auto N : Ti.remove_reference.tuple_size =>
        get<int...(N)>(std::forward<Ti>(tpls...[i]))...
  };
}

Circle Imperative Arguments provides control flow within template argument lists, function argument lists and initializer lists. argument-for is the tool of choice here. To form the function's return type, we argument-for inside the tuple's template-argument-list. For each parameter Ti in Tuples, expand the pack Ti.remove_reference.tuple_elements. This is a usage of Circle member traits, which rewrites C++ type traits using a member-like syntax for clarity. tuple_elements yields a parameter pack by querying std::tuple_size for the pack size and probing std::tuple_elements for each pack member.

The function's body is just the return statement with a hulked out initializer list. This uses the two-declaration version of argument-for, where the first declaration i is the index of iteration, and the second declaration typename Ti holds the current type in the collection Tuples. For each type parameter, we use argument-let to declare a value N, which is set with the number of tuple elements. Finally, there's a pack expansion expression that forwards the ith function parameter tpls...[i] using its forwarding reference Ti into a get function, specialized on each integer between 0 and N - 1. That final line essentially blows out a tuple into its elements.

template<class... Tuples>
constexpr tuple<
  for typename Ti : Tuples => 
    Ti.remove_reference.tuple_elements...
>
tuple_cat2(Tuples&&... tpls) {
  return { 
    for i, typename Ti : Tuples =>
      std::forward<Ti>(tpls...[i])...
  };
}

But this function is even easier with Circle's first-class tuple support. We don't have to form a call to get to destructure each function parameter into the initializer-list. We can simply use the implicit slice syntax to turn the operand into a pack of elements, and expand that right into the return statement's initializer-list.