Search code examples
c++templatesc++17

Invoke functions that return a mix of rvalues and lvalue tuple without the lvalue references decaying into values?


Minimum Example:

struct RValue {
  int x = 12;
  auto operator*() const { return x; }
} x;

struct LValueRef {
  int y = 13;
  auto &operator*() { return y; }
} y;

auto z = std::make_tuple(x, y);

auto f() {
  // Replace std::make_tuple with what? 
  return std::apply([](auto &...x) { return std::make_tuple(*x...); }, z);
}

int main() {
    auto [a, b] = f();
}

I would like f to return a std::tuple<int, int&>, where it is currently returning a std::tuple<int, int>. This is supposed to run on an arbitrary tuple z, where elements on de-referencing can give r-values and l-value references.

Originally, all elements of the tuple z were similar to the LValueRef class, so I used std::tie to return the tuple in the form required.

I essentially need to call std::make_tuple, but wrap a std::ref around the elements which are originally l-vale references. This could be done as the following pseudo code shows:

  if constexpr (std::is_lvalue_reference<T>::value)
    return std::ref(t);
  else
    return t;

I don't know exactly how to write this in the std::apply invocation, but maybe I can transform the types of the std::tuple and create return return std::tuple<Ts_transformed...>(*x...);

What is the ideal way to get this behavior?


I briefly tried to use std::forward_as_tuple without understanding how it works (it compiled). this caused r-values to become r-value references and l-values to become l-value references (i.e., returns std::tuple<int &&, int &>. However, when using this, I believe I was accessing a temporary after it went out of scope, and that isn't correct. I'm not sure what exactly is happening here, and why I cannot use the int && value?


I was originally trying to write an adaptor similar to Python's range and zip. The function I am trying to write above de-references a tuple of iterators. Originally the iterators all returned lvalue-references, so std::tie worked fine, but I wanted this to also work with iterators that return rvalues.

template <typename... WrappedRanges> class ZipWrappedRange {
  std::tuple<WrappedRanges...> r_;
public:
  class iterator {
    std::tuple<typename WrappedRanges::iterator...> its_;
  public:
    explicit iterator(typename WrappedRanges::iterator &&...it) : its_(std::make_tuple(it...)) {}
    explicit iterator(std::tuple<typename WrappedRanges::iterator...> &&it) : its_(it) {}

    auto operator*() const {
      // I need to modify this to allow for cases where (*x) returns rvalues and not just l-value references.
      return std::apply([](auto &...x) { return std::tie((*x)...); }, its_);
    }
  };
  ...
}

Solution

  • std::make_tuple might be replaced with tuple constructor where you provide types explicitly:

    auto f() {
      return std::apply([](auto&...x) { return std::tuple<decltype(*x)...>{*x...}; },
                        z);
    }
    

    Demo