tuplet is a one-header library that implements a fast and lightweight tuple type, tuplet::tuple, that guarantees performance, fast compile times, and a sensible and efficent data layout. A tuplet::tuple is implemented as an aggregate containing it's elements, and this ensures that it's
- trivially copyable,
 - trivially moveable,
 - trivially assignable,
 - trivially constructible,
 - and trivially destructible.
 
This results in better code generation by the compiler, allowing tuplet::tuple to be passed in registers, and to be serialized and deserialized via memcpy.
If you'd like a further discussion of how tuplet::tuple compares to std::tuple and why you should use it, see the Motivation section below!
Creating a tuple is as simple as 1, 2, "Hello, world"! Writing
tuplet::tuple tup = {1, 2, std::string("Hello, world!")};Will create a tuple of type tuple<int, int, std::string>, just like you'd expect it to. This is all you need to get started, but the following sections will expand upon the functionality provided by tuplet in greater depth.
You can access members via get:
std::cout << get<2>(tup) << std::endl; // Prints "Hello, world!"Or via operator[]:
using tuplet::tag; std::cout << tup[tag<2>()] << std::endl; // Prints "Hello, world!"Something that's important to note is that tag is just an alias for std::integral_constant:
template <size_t I> using tag = std::integral_constant<size_t, I>;You can access elements of a tuple very cleanly by using the _tag literal provided in tuplet::literals! This namespace defines the literal operator _tag, which take number and produce a tuplet::tag templated on that number, so 0_tag evaluates to tuplet::tag<0>(), 1_tag evaluates to tuplet::tag<1>(), and so on!
using namespace tuplet::literals; tuplet::tuple tup = {1, 2, std::string("Hello, world!")}; std::cout << tup[0_tag] << std::endl; // Prints 1 std::cout << tup[1_tag] << std::endl; // Prints 2 std::cout << tup[2_tag] << std::endl; // Prints Hello WorldThe tuple can also be accessed via a structured binding:
// a binds to get<0>(tup), // b binds to get<1>(tup), and // c binds to get<2>(tup) auto& [a, b, c] = tup; std::cout << c << std::endl; // Print "Hello, world!"You can create a tuple of references with tuplet::tie! This function acts just like std::tie does:
int a; int b; std::string s; // Creates a tuplet::tuple<int&, int&, std::string&> tuplet::tuple tup = tuplet::tie(a, b, s); // a will be set to 1, // b will be set to 2, and // s will be set to "Hello, world!" tup = tuplet::tuple{1, 2, "Hello, world!"}; std::cout << s << std::endl; // Prints Hello WorldIt's possible to easily and efficently assign values to a tuple using the .assign() method:
tuplet::tuple<int, int, std::string> tup; tup.assign(1, 2, "Hello, world!");You can use std::ref to store references inside a tuple!
std::string message; // t has type tuple<int, int, std::string&> tuplet::tuple t = {1, 2, std::ref(message)}; message = "Hello, world!"; std::cout << get<2>(t) << std::endl; // Prints Hello, world!You can also store a reference by specifying it as part of the type of the tuple:
// Stores a reference to message tuplet::tuple<int, int, std::string&> t = {1, 2, message};These methods are equivilant, but the one with std::ref can result in cleaner and shorter code, so the template deduction guide accounts for it.
As with std::apply, you can use tuplet::apply to use the elements of a tuple as arguments of a function, like so:
// Prints arguments on successive lines auto print = [](auto&... args) { ((std::cout << args << '\n') , ...); }; apply(print, tuplet::tuple{1, 2, "Hello, world!"});tuplet has been backported to C++17. Functions that were constrained with requires clauses will still be constrained in C++20, with sfinae being used where necessary if concepts are not availible.
Tuplet remains trivially copyable and trivially movable, with no user-provided copy or move constructors.
tuplet::tuple provides the following operations on the elements of a tuple:
tuple.any(func)- returns true if the function returns true for any of the tuple's elements.tuplet.all(func)- returns true if the function returns true for all of the tuple's elementstuplet.map(func)- returns a new tuple, whose elements consist of the values returned by the function when it's applied to each element of the tuple separatelytuplet.for_each- applies a function to each element in a tuple, discarding the value
These are bulk operations, and they'll compile significantly faster than lookup with std::get for large tuples.
Additionally, tuplet now supports heterogenous comparisons - you can compare a tuple<int> with tuple<int&>, or with tuple<long>. This can be useful when writing test code.
You can use tuplet::convert to convert a tuplet to other arbitrary compatible types:
struct my_struct { int a; double b; std::string_view c; }; auto tup = tuplet::tuple { 1, 0.3, "Hello world" }; my_struct s = tuplet::convert { tup };Any type that can be constructed with braced-initialization from the elements of a tuple is considered compatible. For tuples of appropriate types, this includes vectors, arrays, structs, and other class types.
If the tuple is moved into tuplet::convert, then any values in the tuple will be moved into the created object.
Tuplet can now be installed as a CMake package!
git clone https://github.com/codeinred/tuplet.git cd tuplet cmake -B build -DCMAKE_INSTALL_PREFIX="/path/to/install" cmake --build build cmake --build build --target installIf you're installing tuplet globally, you may have to run the final command with sudo:
# Global install git clone https://github.com/codeinred/tuplet.git cd tuplet cmake -B build cmake --build build sudo cmake --build build --target installThis will attempt to build tests. If the default system compiler doesn't support C++20 and buliding fails, you can use an alternative compiler by specifying -DCMAKE_CXX_COMPILER during the configuration step:
cmake -B build -DCMAKE_CXX_COMPILER=g++-11Alternatively, on newer versions of CMake (e.g, cmake 3.15 and above), you can skip the build step entirely. See this documentation for more information.
git clone https://github.com/codeinred/tuplet.git cd tuplet cmake -B build sudo cmake --install build # Or: cmake --install build --prefix "/path/to/install"Once tuplet is installed, it can now be discovered via find_package, and targeted via target_link_libraries. It's a header-only library, but this will ensure that tuplet's directory is added to the include path.
cmake_minimum_required(VERSION 3.14) project(my_project LANGUAGES CXX) find_package(tuplet REQUIRED) add_executable(main) target_sources(main PRIVATE main.cpp) target_link_libraries(main PRIVATE tuplet::tuplet)You can install tuplet using the Conan package manager. Add tuplet/1.2.2 to your conanfile.txt's require clause. This way you can integrate tuplet with any build system Conan supports.
This section intends to address a single fundamental question: Why would I use this instead of std::tuple?
It is my hope that by addressing this question, I might explain my purpose for writing this library, as well as providing a clearer overview of what it provides.
std::tuple is not a zero-cost abstraction, and using it introduces a runtime penalty in comparison to traditional aggregate datatypes, such as structs. std::tuple also compiles slowly, introducing a penalty on libraries that make extensive use of it.
tuplet::tuple has none of these problems.
-  
tuplet::tuplean aggregate type.- When the elements are trivially constructible, 
tuplet::tupleis trivially constructible - When elements are trivially destructible, 
tuplet::tupleis trivially destructible 
 - When the elements are trivially constructible, 
 -  
tuplet::tuplecan be passed in the registers. This means that there's's no overhead compared to a struct -  
Compilation is much faster, especially for larger or more complex tuples.
This occurs because
tuplet::tupleis an aggregate type, and also because indexing was specifically designed in a way that allowed for faster lookp of elements. -  
tuplet::tupletakes advantage of empty-base-optimization and[[no_unique_address]]. This means that empty types don't contribute to the size of the tuple. 
Not without both an ABI break and a change to it's API. There are a few reasons for this.
- The memory layout of 
std::tupletends to be in reverse order when compared to a corresponding struct containing the same types. Fixing this would be an ABI break. - Because 
std::tupleisn't trivially copyable and isn't an aggregate, it tends to be passed on the stack instead of in the registers. Fixing this would be an ABI break. - The constructor of 
std::tupleprovides overloads for passing an allocator to the constructor. Given thatstd::tupleshould allocate on the stack, I don't know why this was put into the standard. 
Having an allocator makes sense for a type like std::vector, which was designed for use even in ususual memory-constrained situations, but in my opinion, std::tuple would have been better off with an API that was as simple as possible.
I hope that either a future version of C++ introduces epochs (or a similar feature), which would allow for a re-write of std::tuple; or that some future version introduces a language-level tuple construct, rendering std::tuple obsolete in it's entirety.
Other weird std::tuple facts: When using the MSVC standard library implementation, std::tuple won't even necessarily have the same size as a struct with the same member types. This caused a compile error when I introduced a static_assert that (incorrectly) assumed std::tuple would be sensibly sized. I had to disable the static_assert for MSVC:
// In bench-heterogenous.cpp using hetero_std_tuple_t = std::tuple<int8_t, int8_t, int16_t, int32_t>; using hetero_tuplet_tuple_t = tuplet::tuple<int8_t, int8_t, int16_t, int32_t>; // For some reason this doesn't apply in windows #ifndef _MSC_VER static_assert(sizeof(hetero_std_tuple_t) == 8, "Expected std::tuple to be 8 bytes"); #endif static_assert(sizeof(hetero_tuplet_tuple_t) == 8, "Expected tuplet::tuple to be 8 bytes");Needless to say, being an aggregate type, tuplet::tuple does not suffer from this problem.
The compiler is signifigantly better at optimizing memory-intensive operations on tuplet::tuple when compared to std::tuple, with a measured speedup of 2x when copying vectors of 256 elements, and a speedup up 2.25x for vectors of 512 elements containing homogenous tuples (tuples where all types are identical, test size 8 bytes per element).
Furthermore, for tuples containing more than one type of element (heterogenous tuples, test size 8 bytes per element), speedups as large as 13.35x were observed with tuplet::tuple when compared to std::tuple!
In these benchmarks, the v<n> suffix measures the time to copy a vector containing n elements, each of which is a tuple. You can view the code in the bench/ folder of the repository. It uses the Google Benchmark library.
Why the speedup? As stated before, tuplet::tuple is an aggregate type. This means that the compiler is better able to judge what type of optimizations it's allowed to do. In the case of the copy benchmarks, the compiler is able to implement the copy operation using an memcpy-like operation for tuplet::tuple. This can't be done for std::tuple, however, because std::tuple isn't an aggregate type, and isn't trivially copyable.
To run the benchmarks on your local machine, simply clone and build the project with a compiler that supports either C++17 or C++20. It's been tested on GCC 7 and above, and on Visual Studio 16.1.2 and above (this corresponds to _MSC_VER 1921):
git clone https://github.com/codeinred/tuplet.git cd tuplet cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release cmake --build build build/bench
