Reputation: 10911
So given the following 2 functions:
int rvalue();
int& lvalue();
The following would be valid:
std::tuple<int&, int> x = appropriate_fn(lvalue(), rvalue());
I was thinking something like it like this:
template <typename T, typename...Ts>
auto make_comparible(T const& arg, Ts&&...args)
{
return std::make_tuple(T(arg), make_comparible(args...));
}
template <typename T, typename...Ts>
auto make_comparible(T& arg, Ts&&...args)
{
return std::make_tuple<T&>(arg, make_comparible(args...));
}
template <typename T>
auto make_comparible(T const& arg)
{
return std::tuple<T>(T(arg));
}
template <typename T>
auto make_comparible(T& arg)
{
return std::tuple<T&>(arg);
}
But there's three issues this that I can see.
This is not a simple std::tuple
, but a nested one. Which, come to think about it, may not be an issue as I just want to do comparisons (less than, equal to) on it and should still work.
This doesn't distinguish between a temporary and a const reference. This is a bit annoying but I don't see any way around it.
Most importantly, it doesn't work. Given the following:
std::tuple<int, std::tuple<int&>> x = make_comparible(rvalue(), lvalue());
std::tuple<int&, std::tuple<int>> y = make_comparible(lvalue(), rvalue());
The first one is works, but the second one give an error because make_comparible()
is returning std::tuple<int, std::tuple<int&>>
instead of std::tuple<int&, std::tuple<int>>
. Demo
So, is what I'm asking for possible, or is it a pipe dream?
The use case is that I want to define a function in a class that will return a tuple (or otherwise comparable type), which will not result in a dangling pointer/reference, and which will be easy to use.
Ok, so after watching C++ and Beyond 2012: Scott Meyers - Universal References in C++11, it looks like overloading on a universal reference is almost always an error. But as I am trying to differentiate a lvalue from an rvalue, irrespective of constness, this would be the right way to go.
I can use overloading to get the universal reference to bind to only rvalues, if I declare overloads which are for lvalues. I had also forgotten to use std::forward()
which was a brain fart on my part. I should have known better.
#include <iostream>
#include <tuple>
// forward declarations
template <typename T, typename...Ts>
decltype(auto) make_comparible(T const& arg, Ts&&...args);
template <typename T, typename...Ts>
decltype(auto) make_comparible(T& arg, Ts&&...args);
template <typename T>
decltype(auto) make_comparible(T&& arg);
template <typename T>
decltype(auto) make_comparible(T const& arg);
template <typename T>
decltype(auto) make_comparible(T& arg);
// rvalue
template <typename T, typename...Ts>
decltype(auto) make_comparible(T&& arg, Ts&&...args)
{
std::cout << "rvalue ";
// want to copy, so do not use std::move()
return std::make_tuple(arg, make_comparible(std::forward<Ts>(args)...));
}
// lvalue const
template <typename T, typename...Ts>
decltype(auto) make_comparible(T const& arg, Ts&&...args)
{
std::cout << "lvalue const ref ";
// This is a reference, so store as a reference
return std::make_tuple<T const&>(arg, make_comparible(std::forward<Ts>(args)...));
}
// lvalue
template <typename T, typename...Ts>
decltype(auto) make_comparible(T& arg, Ts&&...args)
{
std::cout << "lvalue ref ";
// This is a reference, so store as a reference
return std::make_tuple<T&>(arg, make_comparible(std::forward<Ts>(args)...));
}
// rvalue
template <typename T>
decltype(auto) make_comparible(T&& arg)
{
std::cout << "rvalue ";
// want to copy, so do not use std::move()
return std::tuple<T>(arg);
}
// lvalue const
template <typename T>
decltype(auto) make_comparible(T const& arg)
{
std::cout << "lvalue const ref ";
// This is a reference, so store as a reference
return std::tuple<T const&>(arg);
}
// lvalue
template <typename T>
decltype(auto) make_comparible(T& arg)
{
std::cout << "lvalue ref ";
// This is a reference, so store as a reference
return std::tuple<T&>(arg);
}
int var = 5;
int rvalue() { return 4; }
int& lvalue() { return var; }
int const& const_lvalue() { return var; }
int main()
{
// expect output "rvalue lvalue ref", OK
std::tuple<int, std::tuple<int&>> x = make_comparible(rvalue(), lvalue());
std::cout << std::endl;
// expect output "rvalue lvalue const ref", OK
std::tuple<int, std::tuple<int const&>> y = make_comparible(rvalue(), const_lvalue());
std::cout << std::endl;
// expect output "lvalue ref lvalue const ref rvalue", OK
make_comparible(lvalue(), const_lvalue(), rvalue());
// But this doesn't work. Type returned was std::tuple<int, std::tuple<int, std::tuple<int> > >. WHY?
std::tuple<int&, std::tuple<int const&, std::tuple<int>>> z = make_comparible(lvalue(), const_lvalue(), rvalue());
std::cout << std::endl;
return 0;
}
So the code path is correct. But the type returned is wrong. I'm getting a std::tuple<int, std::tuple<int, std::tuple<int>>>
instead of a std::tuple<int&, std::tuple<int const&, std::tuple<int>>>
. WHY?
Upvotes: 3
Views: 289
Reputation: 460
Based on @max66's answer, I created a class which stores constructor arguments as a tuple member variable (lvalues as references and rvalues are moved):
#include <type_traits>
#include <tuple>
#include <functional>
#include <utility>
int rVal() { return 0; }
int& lVal() { static int val{1}; return val; }
template<typename T>
struct add_reference_wrapper{ using type = T; };
template<typename T>
struct add_reference_wrapper<T&> : public add_reference_wrapper<std::reference_wrapper<T>>{};
template<typename T>
using add_reference_wrapper_t = typename add_reference_wrapper<T>::type;
template<typename T>
struct remove_reference_wrapper{ using type = T; };
template<typename T>
struct remove_reference_wrapper<std::reference_wrapper<T>> : public remove_reference_wrapper<T&>{};
template<typename T>
using remove_reference_wrapper_t = typename remove_reference_wrapper<T>::type;
template<typename... Args>
struct S{
S(Args&&... args_) : args{std::forward<Args>(args_)...} {}
std::tuple<remove_reference_wrapper_t<Args>...> args;
};
template<typename... Args>
S(Args&&...) -> S<add_reference_wrapper_t<Args>...>;
int main() {
S s{rVal(), lVal()};
static_assert(std::is_same_v<decltype(s.args), std::tuple<int, int&>>);
}
Upvotes: 0
Reputation: 66230
If I understand correctly what do you want, you can use std::reference
(to wrap a l-value reference so that std::make_tuple()
produce std::tuple
with a reference in the corresponding position), and std::forward
, to get the correct type of reference from a variadic list of arguments.
So you can write a couple of convert functions
int rVal ()
{ return 0; }
int & lVal ()
{ static int val { 1 }; return val; }
and make_comparable()
become
template <typename ... Ts>
auto make_comparible (Ts && ... args)
{ return std::make_tuple(convert(std::forward<Ts>(args))...); }
if you can use C++14/C++17 (auto
return type), or
template <typename ... Ts>
auto make_comparible (Ts && ... args)
-> decltype(std::make_tuple(convert(std::forward<Ts>(args))...))
{ return std::make_tuple(convert(std::forward<Ts>(args))...); }
or also (simpler)
template <typename ... Ts>
auto make_comparible (Ts && ... args)
-> decltype(std::make_tuple(convert(std::forward<Ts>(args))...))
{ return { convert(std::forward<Ts>(args))... }; }
if you must use C++11 (auto
plus decltype()
; ugly but works).
The following is a full working (C++14) example.
#include <tuple>
#include <functional>
int rVal ()
{ return 0; }
int & lVal ()
{ static int val { 1 }; return val; }
template <typename T>
std::reference_wrapper<T> convert (T & t)
{ return t; }
template <typename T>
T convert (T && t)
{ return std::move(t); }
template <typename ... Ts>
auto make_comparible (Ts && ... args)
{ return std::make_tuple(convert(std::forward<Ts>(args))...); }
int main ()
{
auto t = make_comparible(rVal(), lVal());
static_assert(std::is_same<std::tuple<int, int&>, decltype(t)>{}, "!");
}
Upvotes: 2