codeinred / tuplet

A fast, simple tuple implementation that implements tuple as an aggregate
Boost Software License 1.0
200 stars 16 forks source link

Empty tuple not supported #4

Closed bugwelle closed 3 years ago

bugwelle commented 3 years ago

Hi,

first of all: Great library! I've learned a few C++ tricks here and there while reading the code.

I wanted to try this library as a replacement for std::tuple in my code base to check if it improves (compile) performance. Unfortunately, there are a few minor to major differences to std::tuple that make it difficult to use it as a drop-in replacement.

One case I heavily rely on in my code base is the use of empty tuples.

Example:

See https://godbolt.org/z/ojYM79q3K

tuplet::tuple<> empty_fails;

Motivation

I've written some generic code that stores lambdas in tuples. And I've got some code that can append other lambdas to this tuple (and a new tuple is returned). Because it's quite generic, it may happen that one tuple is empty.

How to fix

A quick&dirty fix I've used is to add something like this:

template<>
struct tuple<> {
    constexpr static size_t N = 0;

    template <other_than<tuple> U> 
    constexpr auto& operator=(U&& tup)
    {
        // We can't really assign empty tuples, can we?
        return *this;
    }

    template <typename... U>
    constexpr auto& assign(U&&... )
    {
        // Also not really needed.
        return *this;
    }
};
codeinred commented 3 years ago

Hi @bugwelle,

Thank you so much for bringing this up! I pushed a fix to the main branch that addresses this issue, and added some tests to ensure that empty tuples behave properly. Please let me know if this fixes things for you!

The fix was very similar to what you suggested, although since assign binds each value it takes to each corresponding element of the tuple, it should take no values. Also, base_list should still be there, even though it's now empty. This is useful so that functions like apply work without modification.

template <>
struct tuple<> : tuple_base_t<> {
    constexpr static size_t N = 0;
    using super = tuple_base_t<>;
    using base_list = typename super::base_list;

    template <other_than<tuple> U> // Preserves default assignments
    requires stateless<U>          // Check that U is similarly stateless
    constexpr auto& operator=(U&& tup) noexcept {
        return *this;
    }

    constexpr auto& assign() noexcept {
        return *this;
    }
};

Please let me know if this fixes the issue, or if there are any other changes that need to be made!

Best, Alecto

bugwelle commented 3 years ago

That's great! Thank you very much! :)

In case you're interested, here is a list of things I had to adapt to switch from std::tuple to tuplet::tuple:

Other notes:
I have no idea why, but by using your library, I found quite a few implicit conversion issues where I assigned unsigned integers to a tuple of signed integers, etc. No idea why the compiler didn't warn with std::tuple, though. So that's awesome! :)

I recommend that you use some kind of test framework. Personally, I use Catch2 for tests. You don't have a lot of tests, yet, and one file for each test is too much in my opinion. I would also add a lot of tests for tuplet::get where the tuplet is const, non-const, where its values are const, non const, etc. Doing so for my own library yielded many issues because I simply didn't test constness before.

Regards, Andre

codeinred commented 3 years ago

Hi Andre,

I truly appreciate your response, and the effort and clarity you put into laying out improvements that could be made to the library in order to make transition as simple and painless as possible.

One possible alternative: if std::get is replaced with just get, you can rely on ADL to automatically choose between std::get for types in the standard library, and get for user-defined types (like tuplet::tuple). [You can see a live example here!](https://godbolt.org/#g:!((g:!((g:!((h:codeEditor,i:(filename:'1',fontScale:14,fontUsePx:'0',j:1,lang:c%2B%2B,selection:(endColumn:1,endLineNumber:10,positionColumn:1,positionLineNumber:10,selectionStartColumn:1,selectionStartLineNumber:10,startColumn:1,startLineNumber:10),source:'%23include+%3Ctuple%3E%0A%23include+%3Chttps://raw.githubusercontent.com/codeinred/tuplet/main/include/tuplet/tuple.hpp%3E%0A%23include+%3Ciostream%3E%0A%0Aauto%26+get_first(auto%26+tup)+%7B%0A++++return+get%3C0%3E(tup)%3B%0A%7D%0A%0Aint+main()+%7B%0A%0A++++std::tuple+t1+%7B+10,+20%7D%3B%0A++++tuplet::tuple+t2+%7B%22Hello%22,+%22world%22%7D%3B%0A++++std::array+arr+%7B+0.5,+0.6,+0.7%7D%3B%0A%0A++++std::cout+%3C%3C+get_first(t1)+%3C%3C+!'%5Cn!'%3B%0A++++std::cout+%3C%3C+get_first(t2)+%3C%3C+!'%5Cn!'%3B%0A++++std::cout+%3C%3C+get_first(arr)+%3C%3C+!'%5Cn!'%3B%0A%7D'),l:'5',n:'0',o:'C%2B%2B+source+%231',t:'0')),k:50,l:'4',n:'0',o:'',s:0,t:'0'),(g:!((h:compiler,i:(compiler:g112,filters:(b:'0',binary:'1',commentOnly:'0',demangle:'0',directives:'0',execute:'0',intel:'0',libraryCode:'0',trim:'1'),flagsViewOpen:'1',fontScale:14,fontUsePx:'0',j:1,lang:c%2B%2B,libs:!(),options:'-std%3Dc%2B%2B20',selection:(endColumn:1,endLineNumber:1,positionColumn:1,positionLineNumber:1,selectionStartColumn:1,selectionStartLineNumber:1,startColumn:1,startLineNumber:1),source:1,tree:'1'),l:'5',n:'0',o:'x86-64+gcc+11.2+(C%2B%2B,+Editor+%231,+Compiler+%231)',t:'0'),(h:output,i:(compiler:1,editor:1,fontScale:14,fontUsePx:'0',tree:'1',wrap:'1'),l:'5',n:'0',o:'Output+of+x86-64+gcc+11.2+(Compiler+%231)',t:'0')),k:50,l:'4',n:'0',o:'',s:0,t:'0')),l:'2',n:'0',o:'',t:'0')),version:4)

// Because of ADL, this will work for both std::tuple and tuplet::tuple, as well as std::array and std::pair.
auto& get_first(auto& tup) {
    return get<0>(tup);
}

Pull request

Could you create a pull request to implement tuplet::forward_as_tuple and tuplet::make_tuple? If not, I'll just add them myself, but it's nice to get contributions from people!

Conversion

I opted not to include implicit conversion, however if you want to convert between tuples you can do it via tuplet::convert!

int main() {
    tuplet::tuple<short, int> t1{5, 10};

    // tuplet::convert handles conversion!
    std::tuple<int, int> t2 = tuplet::convert(t1);
    std::pair<long, float> t3 = tuplet::convert(t1);
    std::array<int, 2> t4 = tuplet::convert(t1);
}

Note on fast compile-time performance

I plan on writing and publishing a longer explanation of why std::tuple compiles slowly, however the TL;DR is that when working with all the values in a tuple, tuple::base_list should be preferred to get because looking up a value corresponding to a particular base class is significantly faster than overload resolution (which has to be done under the hood when using get). This isn't a limitation of my library; it applies to the standard library too, and it's one of the reasons std::tuple compiles slowly.

On the dev/bench-compile branch of this project, I have a script benchmarking the compile time of apply for both std::tuple and tuplet::tuple, and tuplet achieves a speedup of dozens to hundreds of times for large numbers of elements. (It's faster in all cases, including for small numbers, but large numbers are where it really shines.)

You can benchmark it on your own machine by running:

git clone https://github.com/codeinred/tuplet.git
cd tuplet
git checkout dev/bench-compile
cd bench-compile-times
./bench-apply.sh 10 450

The output should look something like this. the number in the first column is the number of elements in the tuple, the number in the second column is the time to compile, and the number in the third column is the memory usage during compilation. The compile time of apply on std::tuple grows at about O(n^2) relative to the number of elements, whereas for tuplet itsO(n log(n))`. There's a minimum time of about 0.08s in the below data, but that's just the time it takes the compiler to load the header files.

Using g++ aka g++ (GCC) 11.2.0
tuplet::tuple: 10, 0.08, 31872
std::tuple:    10, 0.15, 46256
tuplet::tuple: 20, 0.08, 32468
std::tuple:    20, 0.18, 55144
tuplet::tuple: 30, 0.08, 32884
std::tuple:    30, 0.22, 67344
tuplet::tuple: 40, 0.08, 33324
std::tuple:    40, 0.28, 83940
tuplet::tuple: 50, 0.09, 34196
std::tuple:    50, 0.33, 103764
tuplet::tuple: 60, 0.09, 34408
std::tuple:    60, 0.41, 125948
tuplet::tuple: 70, 0.09, 35236
std::tuple:    70, 0.49, 162056
tuplet::tuple: 80, 0.09, 35716
std::tuple:    80, 0.60, 201336
tuplet::tuple: 90, 0.09, 36248
std::tuple:    90, 0.72, 243452
tuplet::tuple: 100, 0.09, 36752
std::tuple:    100, 0.87, 289756
tuplet::tuple: 110, 0.10, 36980
std::tuple:    110, 1.08, 334568
tuplet::tuple: 120, 0.10, 37660
std::tuple:    120, 1.30, 388500
tuplet::tuple: 130, 0.10, 38444
std::tuple:    130, 1.52, 467996
bugwelle commented 3 years ago

Dear Alecto,

thank you very much for your response and all of your effort. I very much appreciate it! :)

  • Regarding the lack of a constructor, this was an intentional choice. In order for tuplet::tuple to be an aggregate type, it has to forgo user-declared constructors. While this situation isn't ideal, it provides really good runtime characteristics, especially because values can be constructed in-place rather than being moved.

Could you expand on that a little bit or maybe point me to some resources in that regard? How are the runtime characteristics better? I'm interested in that topic as it may be useful for my own projects.

  • Regarding std::get for tuplet: I'm not sure if it's permitted to overload std::get for user-defined types. I'll do some research on this, and if it's permitted by the standard, then I'll provide an overload!

Raymond Chen does it here. That's what I read when I wanted to have structured bindings for my own types. If you find that it's not allowed by the standard, please let me know. :)

One possible alternative: if std::get is replaced with just get, you can rely on ADL to automatically choose between std::get for types in the standard library, and get for user-defined types (like tuplet::tuple).

ADL is (sometimes) still magic to me and I had at least one bug in the past due to ADL which is why I try to avoid it. :)

Could you create a pull request to implement tuplet::forward_as_tuple and tuplet::make_tuple? If not, I'll just add them myself, but it's nice to get contributions from people!

Sure! :)

I plan on writing and publishing a longer explanation of why std::tuple

That would be great! :)

I opted not to include implicit conversion

Which is great IMHO. Thank you for clarifying this. May I hijack this thread to ask you how this detail in your tuple_cat works?

std::forward<Tup>(t).identity_t<Bases>::value

Where does indentity_t come from? I see it's implementation at the beginning of tuple.hpp but I don't see it used anywhere except in usage such as above. Is identity_t a member variable in some tuple?

bugwelle commented 3 years ago

I just compared compile times of my projects for std::tuple vs tuplet::tuple.

Setup

CMakePresets.json:

{
  "version": 2,
  "cmakeMinimumRequired": {
    "major": 3,
    "minor": 20,
    "patch": 0
  },
  "configurePresets": [
    {
      "name": "test",
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/test",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Release",
        "CMAKE_CXX_FLAGS": "-march=native",
        "CMAKE_CXX_COMPILER": "g++",
        "FUSION_BUILD_TEST": "ON",
        "FUSION_BUILD_EXAMPLES": "ON"
      }
    }
  ],
  "buildPresets": [
    {
      "name": "test",
      "configurePreset": "test"
    }
  ]
}

Compile Commands:

rm -rf build/test
cmake --preset test
time cmake --build --preset test

Note that I'm using ninja which uses all of my 24 cores.

Results

# With Tuplet
cmake --build --preset test  379,97s user 17,20s system 2075% cpu 19,132 total
cmake --build --preset test  380,02s user 16,82s system 2073% cpu 19,138 total
cmake --build --preset test  381,08s user 16,74s system 2033% cpu 19,559 total
cmake --build --preset test  383,23s user 16,99s system 2072% cpu 19,307 total
cmake --build --preset test  383,51s user 17,16s system 2098% cpu 19,092 total

# No Tuplet
cmake --build --preset test  409,68s user 17,09s system 2169% cpu 19,673 total
cmake --build --preset test  409,22s user 17,13s system 2125% cpu 20,063 total
cmake --build --preset test  411,33s user 16,98s system 2106% cpu 20,328 total
cmake --build --preset test  416,20s user 17,53s system 2096% cpu 20,692 total
cmake --build --preset test  412,12s user 17,41s system 2158% cpu 19,897 total

Time spent in user space is 30 seconds less. However, to the user, i.e. to me, there is a difference of maybe about a second.

If I pass -j 1, i.e. only use one thread, the times change a bit:

# With Tuplet
cmake --build --preset test -j 1  222,30s user 10,58s system 99% cpu 3:53,10 total
cmake --build --preset test -j 1  223,55s user 10,56s system 99% cpu 3:54,32 total

# No Tuplet
cmake --build --preset test -j 1  237,27s user 10,08s system 99% cpu 4:07,59 total
cmake --build --preset test -j 1  241,14s user 10,84s system 99% cpu 4:12,25 total

So in single threaded cases, the compile time went down by more than 10 seconds. :)

codeinred commented 3 years ago

Dear Andre,

I'm happy to announce that tuplet now supports comparisons with operator <=> and operator ==, from which the other comparison and equality providers can be obtained automatically.

Thankfully, we were able to default these operators for tuple and it's base type, type_map:

template <class... Bases>
struct type_map : Bases... {
    // ...
    auto operator<=>(type_map const&) const = default;
    bool operator==(type_map const&) const = default;
};
template <class... T>
struct tuple : tuple_base_t<T...> {
    // ...
    auto operator<=>(tuple const&) const = default;
    bool operator==(tuple const&) const = default;
};

Comparison for tuples containing references

tuple types are supposed to provide comparison operators even if they contain references (such as in the case of tuple<int&, int&>, and unfortunately defaulting isn't allowed in these cases. My initial implementation to provide comparison in the case of references was to split tuple_elem into two classes:

Comparison code for tuple_elem ```cpp template struct tuple_elem { // Like declval, but with the element static T decl_elem(tag); using type = T; [[no_unique_address]] T value; constexpr decltype(auto) operator[](tag) & { return (value); } constexpr decltype(auto) operator[](tag) const& { return (value); } constexpr decltype(auto) operator[](tag) && { return (std::move(*this).value); } auto operator<=>(tuple_elem const&) const = default; bool operator==(tuple_elem const&) const = default; }; template struct tuple_elem { // Like declval, but with the element static T decl_elem(tag); using type = T; [[no_unique_address]] T value; constexpr decltype(auto) operator[](tag) & { return (value); } constexpr decltype(auto) operator[](tag) const& { return (value); } constexpr decltype(auto) operator[](tag) && { return (std::move(*this).value); } // Implements comparison for tuples containing reference types constexpr auto operator<=>(tuple_elem const& other) const { return value <=> other.value; } constexpr bool operator==(tuple_elem const& other) const { return value == other.value; } }; ```

However, this didn't handle r-value references, it wasn't clear if it handled const references, and it was rather verbose. Thankfully, I discovered that we could provide an alternative by taking advantage of requires:

Implementation with requires ```cpp template struct tuple_elem { // Like declval, but with the element static T decl_elem(tag); using type = T; [[no_unique_address]] T value; constexpr decltype(auto) operator[](tag) & { return (value); } constexpr decltype(auto) operator[](tag) const& { return (value); } constexpr decltype(auto) operator[](tag) && { return (std::move(*this).value); } auto operator<=>(tuple_elem const&) const = default; bool operator==(tuple_elem const&) const = default; // Implements comparison for tuples containing reference types constexpr auto operator<=>(tuple_elem const& other) const requires(reference) { return value <=> other.value; } constexpr bool operator==(tuple_elem const& other) const requires(reference) { return value == other.value; } }; ```

This worked on MSVC, GCC, and Clang with libstdc++, but it broke when using libc++ (it was weird). It was breaking in tests where comparisons weren't even being used. Thankfully, this could be fixed by further constraining <=> and ==, so that they explicitly required types to provide the appropriate operations:

Fixed code ```cpp template struct tuple_elem { // Like declval, but with the element static T decl_elem(tag); using type = T; [[no_unique_address]] T value; constexpr decltype(auto) operator[](tag) & { return (value); } constexpr decltype(auto) operator[](tag) const& { return (value); } constexpr decltype(auto) operator[](tag) && { return (std::move(*this).value); } auto operator<=>(tuple_elem const&) const = default; bool operator==(tuple_elem const&) const = default; // Implements comparison for tuples containing reference types constexpr auto operator<=>(tuple_elem const& other) const noexcept(noexcept(value <=> other.value)) requires(std::is_reference_v && ordered) { return value <=> other.value; } constexpr bool operator==(tuple_elem const& other) const noexcept(noexcept(value == other.value)) requires(std::is_reference_v && equality_comparable) { return value == other.value; } }; ```

Ultimately, I'm happy with the resulting code, and it looks sensible (despite the need for workarounds). You can see the details of this implementation and it's changes in #8.

Regarding std::get overloading

I looked over Raymond Chen's article, and as far as I can tell, he doesn't overload std::get. He provides a get function, but he does it at the same namespace scope as the Person class (which appears to be the global scope). I posted a thread asking about it on r/cpp. There wasn't a conclusive answer, however the general consensus leaned in the direction of discouraging it. Given the ambiguity, I'd prefer not to include it in the library, however I have two ideas!

  • If you want to use it in your own personal project, you can add it to namespace std with one line of code:
    namespace std { using tuplet::get; }
  • Alternatively, you could include both std::get and tuplet::get in your namespace, and then you won't have to rely on ADL:
    namespace bugwelle {
       using tuplet::get;
       using std::get;
    }

    This second solution is the one I recommend, and it follows the principle of least surprise (it's definitely standards compliant, and it's overall a pretty sensible thing to do)

Regarding the identity_t implementation detail

tuplet contains a lot of code similar to this:

std::forward<Tup>(t).identity_t<Bases>::value

The general case of this syntax is value.BaseClass::member. Each of a tuple's base classes has a value member, so given a list of base classes, we can quickly access all values in the tuple:

template <class F, class Tup, class... Bases>
auto apply_impl(F&& f, Tup&& tup, type_list<Bases...> unused) {
    return f(tup.Bases::value ...); // Get all the values of the tuple
}

[This works in GCC](https://godbolt.org/#g:!((g:!((g:!((h:codeEditor,i:(filename:'1',fontScale:14,fontUsePx:'0',j:1,lang:c%2B%2B,selection:(endColumn:27,endLineNumber:7,positionColumn:27,positionLineNumber:7,selectionStartColumn:27,selectionStartLineNumber:7,startColumn:27,startLineNumber:7),source:'%23include+%3Chttps://raw.githubusercontent.com/codeinred/tuplet/main/include/tuplet/tuple.hpp%3E%0A%23include+%3Ccstdio%3E%0Ausing+namespace+tuplet%3B%0A%0Atemplate+%3Cclass+F,+class+Tup,+class...+Bases%3E%0Aauto+apply_impl(F%26%26+f,+Tup%26%26+tup,+type_list%3CBases...%3E+unused)+%7B%0A++++return+f(tup.Bases::value+...)%3B+//+Get+all+the+values+of+the+tuple%0A%7D%0A%0Aint+main()+%7B%0A++++using+tup+%3D+tuple%3Cint,+int,+int%3E%3B%0A++++using+base_list+%3D+typename+tup::base_list%3B%0A++++auto+add+%3D+%5B%5D(auto...+values)+%7B+return+(values+%2B+...)%3B+%7D%3B%0A++++int+sum+%3D+apply_impl(add,+tup%7B1,2,3%7D,+base_list%7B%7D)%3B%0A%0A++++if+(sum+%3D%3D+6)+%7B%0A++++++++puts(%22Good!!%22)%3B%0A++++%7D+else+%7B%0A++++++++puts(%22Bad%22)%3B%0A++++%7D%0A%7D'),l:'5',n:'0',o:'C%2B%2B+source+%231',t:'0')),k:50,l:'4',n:'0',o:'',s:0,t:'0'),(g:!((h:compiler,i:(compiler:g112,filters:(b:'0',binary:'1',commentOnly:'0',demangle:'0',directives:'0',execute:'0',intel:'0',libraryCode:'0',trim:'1'),flagsViewOpen:'1',fontScale:14,fontUsePx:'0',j:1,lang:c%2B%2B,libs:!(),options:'-std%3Dc%2B%2B20',selection:(endColumn:1,endLineNumber:1,positionColumn:1,positionLineNumber:1,selectionStartColumn:1,selectionStartLineNumber:1,startColumn:1,startLineNumber:1),source:1,tree:'1'),l:'5',n:'0',o:'x86-64+gcc+11.2+(C%2B%2B,+Editor+%231,+Compiler+%231)',t:'0'),(h:output,i:(compiler:1,editor:1,fontScale:14,fontUsePx:'0',tree:'1',wrap:'1'),l:'5',n:'0',o:'Output+of+x86-64+gcc+11.2+(Compiler+%231)',t:'0')),k:50,l:'4',n:'0',o:'',s:0,t:'0')),l:'2',n:'0',o:'',t:'0')),version:4), however [it fails to compile in clang.](https://godbolt.org/#g:!((g:!((g:!((h:codeEditor,i:(filename:'1',fontScale:14,fontUsePx:'0',j:1,lang:c%2B%2B,selection:(endColumn:34,endLineNumber:12,positionColumn:34,positionLineNumber:12,selectionStartColumn:34,selectionStartLineNumber:12,startColumn:34,startLineNumber:12),source:'%23include+%3Chttps://raw.githubusercontent.com/codeinred/tuplet/main/include/tuplet/tuple.hpp%3E%0A%23include+%3Ccstdio%3E%0Ausing+namespace+tuplet%3B%0A%0Atemplate+%3Cclass+F,+class+Tup,+class...+Bases%3E%0Aauto+apply_impl(F%26%26+f,+Tup%26%26+tup,+type_list%3CBases...%3E+unused)+%7B%0A++++return+f(tup.Bases::value+...)%3B+//+Get+all+the+values+of+the+tuple%0A%7D%0A%0Aint+main()+%7B%0A++++using+tup+%3D+tuple%3Cint,+int,+int%3E%3B%0A++++using+base_list+%3D+typename+tup::base_list%3B%0A++++auto+add+%3D+%5B%5D(auto...+values)+%7B+return+(values+%2B+...)%3B+%7D%3B%0A++++int+sum+%3D+apply_impl(add,+tup%7B1,2,3%7D,+base_list%7B%7D)%3B%0A%0A++++if+(sum+%3D%3D+6)+%7B%0A++++++++puts(%22Good!!%22)%3B%0A++++%7D+else+%7B%0A++++++++puts(%22Bad%22)%3B%0A++++%7D%0A%7D'),l:'5',n:'0',o:'C%2B%2B+source+%231',t:'0')),k:50,l:'4',n:'0',o:'',s:0,t:'0'),(g:!((h:compiler,i:(compiler:clang1101,filters:(b:'0',binary:'1',commentOnly:'0',demangle:'0',directives:'0',execute:'0',intel:'0',libraryCode:'0',trim:'1'),flagsViewOpen:'1',fontScale:14,fontUsePx:'0',j:1,lang:c%2B%2B,libs:!(),options:'-std%3Dc%2B%2B20',selection:(endColumn:1,endLineNumber:1,positionColumn:1,positionLineNumber:1,selectionStartColumn:1,selectionStartLineNumber:1,startColumn:1,startLineNumber:1),source:1,tree:'1'),l:'5',n:'0',o:'x86-64+clang+11.0.1+(C%2B%2B,+Editor+%231,+Compiler+%231)',t:'0'),(h:output,i:(compiler:1,editor:1,fontScale:14,fontUsePx:'0',tree:'1',wrap:'1'),l:'5',n:'0',o:'Output+of+x86-64+clang+11.0.1+(Compiler+%231)',t:'0')),k:50,l:'4',n:'0',o:'',s:0,t:'0')),l:'2',n:'0',o:'',t:'0')),version:4)

The use of identity_t is a workaround that forces clang to treat Bases as a type parameter pack, allowing it to be expanded and used with this context. Because identity_t<T> just produces T, this is semantically equivalent to the above usage of tup.Bases::value, and so it works without issue in GCC and MSVC (which happens to suffer from the same issue as clang).

[You can see a working example here.](https://godbolt.org/#g:!((g:!((g:!((h:codeEditor,i:(filename:'1',fontScale:14,fontUsePx:'0',j:1,lang:c%2B%2B,selection:(endColumn:35,endLineNumber:7,positionColumn:35,positionLineNumber:7,selectionStartColumn:35,selectionStartLineNumber:7,startColumn:35,startLineNumber:7),source:'%23include+%3Chttps://raw.githubusercontent.com/codeinred/tuplet/main/include/tuplet/tuple.hpp%3E%0A%23include+%3Ccstdio%3E%0Ausing+namespace+tuplet%3B%0A%0Atemplate+%3Cclass+F,+class+Tup,+class...+Bases%3E%0Aauto+apply_impl(F%26%26+f,+Tup%26%26+tup,+type_list%3CBases...%3E+unused)+%7B%0A++++return+f(tup.identity_t%3CBases%3E::value+...)%3B+//+Get+all+the+values+of+the+tuple%0A%7D%0A%0Aint+main()+%7B%0A++++using+tup+%3D+tuple%3Cint,+int,+int%3E%3B%0A++++using+base_list+%3D+typename+tup::base_list%3B%0A++++auto+add+%3D+%5B%5D(auto...+values)+%7B+return+(values+%2B+...)%3B+%7D%3B%0A++++int+sum+%3D+apply_impl(add,+tup%7B1,2,3%7D,+base_list%7B%7D)%3B%0A%0A++++if+(sum+%3D%3D+6)+%7B%0A++++++++puts(%22Good!!%22)%3B%0A++++%7D+else+%7B%0A++++++++puts(%22Bad%22)%3B%0A++++%7D%0A%7D'),l:'5',n:'0',o:'C%2B%2B+source+%231',t:'0')),k:50,l:'4',n:'0',o:'',s:0,t:'0'),(g:!((h:compiler,i:(compiler:clang1101,filters:(b:'0',binary:'1',commentOnly:'0',demangle:'0',directives:'0',execute:'0',intel:'0',libraryCode:'0',trim:'1'),flagsViewOpen:'1',fontScale:14,fontUsePx:'0',j:1,lang:c%2B%2B,libs:!(),options:'-std%3Dc%2B%2B20',selection:(endColumn:1,endLineNumber:1,positionColumn:1,positionLineNumber:1,selectionStartColumn:1,selectionStartLineNumber:1,startColumn:1,startLineNumber:1),source:1,tree:'1'),l:'5',n:'0',o:'x86-64+clang+11.0.1+(C%2B%2B,+Editor+%231,+Compiler+%231)',t:'0'),(h:output,i:(compiler:1,editor:1,fontScale:14,fontUsePx:'0',tree:'1',wrap:'1'),l:'5',n:'0',o:'Output+of+x86-64+clang+11.0.1+(Compiler+%231)',t:'0')),k:50,l:'4',n:'0',o:'',s:0,t:'0')),l:'2',n:'0',o:'',t:'0')),version:4)

template <class F, class Tup, class... Bases>
auto apply_impl(F&& f, Tup&& tup, type_list<Bases...> unused) {
    return f(tup.identity_t<Bases>::value ...); // Get all the values of the tuple
}

In summary: identity_t is not a member of tuple. identity_t<Bases> just aliases Bases, but by using Bases in this context, clang and MSVC treat it as a parameter pack and allow it to be expanded.

Despite the identity_t workaround, looking up values this way ends up being much faster at compile time for looking up values in a tuple than either get<I> or operator[]. This is because both get<I> and operator[] result in overload resolution, which is more complex operation for the compiler, especially with large numbers of overloads.

About aggregate types

For more information, see here

Provided an aggregate doesn't contain any references, it'll be compatible with a C memory layout. It'll also be memcpyable when the members of the tuple are memcpyable, and you get the copy and move constructors basically for free. You also get copy assignment and move assignment for free. I don't enjoy writing constructors with a bunch of overloads, and the constructor for tuple types is particularly nasty when you don't use an aggregate, because you want to be able to construct types from any elements that can be used to construct them. There's the added benefit that the compiler is very good at producing efficient code for aggregates, as seen here!

Aggregates also compose very nicely - if you have a tuplet::tuple containing two std::arrays you can just pass all the elements together:

tuplet::tuple<std::array<int, 3>, std::array<int, 4>> tup { 1, 2, 3, 4, 5, 6, 7 };

This particular example isn't that useful, but I use the same property in another library I'm working on that relies on tuplet::tuple being an aggregate to simplify the code and to avoid having to write a forwarding constructor.

Also! Aggregates can store types that can't be moved! This isn't super widely applicable, but I think it's neat! See example here. If you swapped tuplet::tuple with std::tuple it wouldn't compile.

codeinred commented 3 years ago

We have make_tuple, forward_as_tuple, tuple_cat, comparison operators, and I think I've addressed the other suggested changes, so I'm going to close this issue! Thank you so much for your help with the project. I really appreciate it! If there's any other issues you encounter, or if you have any suggestions or improvements, please reach out. It was nice working with you!

I still plan on integrating the Catch2 Testing Library, and I'd definitely appreciate your help there! (You can open another issue for that one if you'd like!)

Best, Alecto

bugwelle commented 3 years ago

Hi,

Writing from mobile, let's see if autocorrect works šŸ˜„

I'm happy to announce that tuplet now supports comparisons with operator <=> and operator ==, from which the other comparison and equality providers can be obtained automatically.

Great! Thank you very much!

I looked over Raymond Chen's article, and as far as I can tell, he doesn't overload std::get. He provides a get function, but he does it at the same namespace scope as the Person class (which appears to be the global scope).

Oh God, I'm sorry. Maybe it was some other blog where I saw that. But never mind. I saw the reddit thread and saw that apparently ADL has changed a bit in C++20 of which I wasn't aware. I still dislike writing get without std for some reason, but I'll get used to it. :)

Your second example looks great. I already have everything in my own name space, so that's a great solution. Thank you very much!

The general case of this syntax is value.BaseClass::member. Each of a tuple's base classes has a value member, so given a list of base classes, we can quickly access all values in the tuple:

Cool! I didn't know that works šŸ˜…

In summary: identity_t is not a member of tuple. identity_t<Bases> just aliases Bases, but by using Bases in this context, clang and MSVC treat it as a parameter pack and allow it to be expanded.

Just wow. I have no idea how the compiler knows that indentity_t is not a member but rather a type but that's something I'll look into another time. I'm just amazed that that works.

Aggregates also compose very nicely - if you have a tuplet::tuple containing two std::arrays you can just pass all the elements together:

tuplet::tuple<std::array<int, 3>, std::array<int, 4>> tup { 1, 2, 3, 4, 5, 6, 7 };

That... Is exactly what I need. I'm not joking, this is perfect šŸ˜

codeinred commented 3 years ago

Iā€™m really glad that tuple being an aggregate provided what you needed!

btw!!! tuple_cat is much more efficient now! We found a non-recursive implementation and then figured out how to ensure optimal assembly for both gcc and clang! See #14, #10 and #11!

bugwelle commented 3 years ago

I saw those commits and am already using the latest version. :-)

I like the concept of the tuple of tuples that is then flattened. I used something similar in another library, but with recursive templates. Your implementation is awesome. šŸŽ‰


My use case is a multidimensional range, where each dimension is stored as an aggregate type, e.g.

// simplified
struct Dim {
 int start{};
 int end{};
};

and if I have N dimensions, I have a tuple of N times Dim.

Instead of:

range({0,Z}, {0,Y}, {0,X}); // simplified

I can now write

range({0,Z, 0,Y, 0,X}); // simplified

It may not seem like much, but having an aggregate type, I was able to remove a lot of boilerplate code and template specializations that I had.

I'm currently writing my master's thesis. This "range type" is only a small part of my thesis, but I'm still very happy to have found this. I'd like to cite your project. Do you have any preferences as to how you would like to be cited? :)

I'd go with this bibtex entry:

@software{tuplet_2021,
  author = { Perez, Alecto Irene },
  title = {{tuplet: A Lightweight Tuple Library for Modern C++}},
  url = {https://github.com/codeinred/tuplet},
  version = {1.1.1},
  year = {2021},
  month = {10},
}

See also: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-citation-files

codeinred commented 3 years ago

That would be amazing, thank you so much! I don't have any strong preferences with regard to being cited, and the citation you provided looks good! If you need a git tag I do my best to provide one for each release / updated version!

bugwelle commented 3 years ago

I think version 1.1.1 is enough information in this case. Thank you for all your work. :)