1

I have a type effectively wrapping a variadic std::tuple like this:

#include <iostream>
#include <tuple>

template <typename ...Args>
struct Foo {
    std::tuple<Args...> t;

    Foo(Args&&... a)
        : t{ std::forward<Args>(a)... }
    { }

    Foo& operator +=(const Foo& f) {
        std::apply([&](auto&&... ts) {
            std::apply([...ts = std::forward<decltype(ts)>(ts)](auto&&... fts) {
                ((ts += fts), ...);
            }, f.t);
        }, t);
        return *this;
    }

    friend std::ostream& operator <<(std::ostream& os, const Foo& f) {
        std::apply([&](auto&&... ts) {
            ((os << ts << ' '), ...);
        }, f.t);
        return os;
    }
};

int main() {
    Foo goy{ 1, 2, 3 };
    Foo bar{ 4, 5, 6 };
    goy += bar;
    std::cout << goy << std::endl; // expect 5 7 9 as output
}

Now I want to be able to add-assign instances of this type by adding the tuple entries element-wise, as indicated in the code snippet and preferably with the use of fold-expressions instead of rather clumsy constructions using std::index_sequence<> and alike. However, my attempt fails at the compiler level, with clang facing and internal compiler crash and GCC diagnosing:

 error: assignment of read-only variable 'ts#0'
   15 |                 ((ts += fts), ...);
      |                  ~~~~^~~~~~~

Cf. Godbolt

I fail to see why ts is immutable here. So what's wrong? Is the program ill-formed and/or are the compilers buggy? Is there an elegant fix?

Jodocus
  • 7,493
  • 1
  • 29
  • 45
  • I want to mention that this issue also appears for plain assignment instead of `operator +=`. I guess I must've mixed something up with my fold expression... – Jodocus Jun 08 '22 at 15:58
  • Just a note, but `Args` does not work as expected in the constructor, they are not universal references thre, but always r-value references. Forwarding won't kick in. Consider making the ctor template with its own args. And provide suitable deduction guide, maybe disallowing `Args&` capture. – Quimby Jun 08 '22 at 16:10
  • 1
    @Quimby Good to know, though in my use case them being rvalue references is actually what I need. Using a `std::move` should make this more explicit, though. – Jodocus Jun 08 '22 at 16:18

2 Answers2

3

lambda capture are const by default, you need to add mutable to mutate it.

std::apply([...ts = std::forward<decltype(ts)>(ts)](auto&&... fts) mutable { ... }

although I don't see why you capture it by value here, to me it seems like you actually want to capture them by reference.

std::apply([&](auto&&... fts){ ... }
apple apple
  • 10,292
  • 2
  • 16
  • 36
0

You can implement this by using std::index_sequence and using a helper function. Imho the code you get is easier to understand than using nested std::applys:

template <typename ...Args>
struct Foo
{
    ...

    Foo& operator +=(const Foo& f) {
        AddHelper(f, std::make_index_sequence<sizeof...(Args)>{});
        return *this;
    }

    ...
private:
    template<size_t ... I>
    void AddHelper(Foo const& other, std::index_sequence<I...>)
    {
        ((std::get<I>(t) += std::get<I>(other.t)), ...);
    }
};
fabian
  • 80,457
  • 12
  • 86
  • 114