Reputation: 13320
In C++ we cannot create containers of references:
std::vector<int&> vri;
In instantiation of ‘class __gnu_cxx::new_allocator<int&>’: required from ‘class std::allocator<int&>’ required from ‘struct std::_Vector_base<int&, std::allocator<int&> >’ required from ‘class std::vector<int&>’ required from here error: forming pointer to reference type ‘int&’ typedef _Tp* pointer; ^~~~~~~
The internal implementation requires to create a pointer to the contained type, that leads to the pointer to reference forbidden type.
Luckily, std::reference_wrapper
exists:
int x{1}, y{2}, z{3};
std::vector<std::reference_wrapper<int>> vr{x, y, z};
for (auto &v : vr)
++v;
std::cout << x << ' ' << y << ' ' << z << '\n';
Code above shows 2 3 4
.
I'm working on a C++ filter utility, as example the filter where
receives a container and returns std::reference_wrapper
to the contained objects that meets the criteria:
template <typename container_t> auto range(const container_t &container)
{ return std::tuple{std::begin(container), std::end(container)}; };
template <typename container_t, typename predicate_t>
auto where(const container_t &container, predicate_t predicate)
{
auto [b, e] = range(container);
using type = std::remove_reference_t<decltype(*b)>;
using reference = std::reference_wrapper<type>;
std::vector<reference> result{};
std::copy_if(b, e, std::back_inserter(result), predicate);
return result;
}
Code below shows 2 3 6 7
:
int main()
{
std::vector v{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
for (auto &x : where(v, [](auto n){ return n & 0b10; }))
std::cout << x << ' ';
return 0;
}
But I have a problem chaining filters:
for (const auto &x :
where(where(v, [](auto n){ return n & 0b10; }), [](auto n){ return n & 0b1; })) {
std::cout << x << ' ';
}
no match for ‘operator<<’ (operand types are ‘std::ostream’ {aka ‘std::basic_ostream<char>’} and ‘const std::reference_wrapper<const std::reference_wrapper<const int> >’) std::cout << x << ' '; ~~~~~~~~~~^~~~
Inner where
returns std::vector<std::refernce_wrapper<int>>
, so the outer one will use std::vector<std::refernce_wrapper<const std::refernce_wrapper<const int>>>
.
In order to solve the problem, I've tried to create a template that unwraps std::reference_wrapper<T>
:
template <typename type_t>
struct unwrap
{
using type = type_t;
};
template <typename type_t>
struct unwrap<std::reference_wrapper<type_t>>
{
using type = type_t;
};
template <typename type_t>
using unwrap_t = typename unwrap<type_t>::type;
So far, it looks like it's working:
int main()
{
using ri = std::reference_wrapper<int>;
using rf = std::reference_wrapper<float>;
using rri = std::reference_wrapper<ri>;
using rrri = std::reference_wrapper<rri>;
std::cout
<< typeid(int).name() << '\t' << typeid(unwrap_t<int>).name() << '\n'
<< typeid(float).name() << '\t' << typeid(unwrap_t<float>).name() << '\n'
<< typeid(ri).name() << '\t' << typeid(unwrap_t<ri>).name() << '\n'
<< typeid(rf).name() << '\t' << typeid(unwrap_t<rf>).name() << '\n'
<< typeid(rri).name() << '\t' << typeid(unwrap_t<rri>).name() << '\n'
<< typeid(rrri).name() << '\t' << typeid(unwrap_t<rrri>).name();
return 0;
}
It yields the propper mangled names:
i i f f St17reference_wrapperIiE i St17reference_wrapperIfE f St17reference_wrapperIS_IiEE St17reference_wrapperIiE St17reference_wrapperIS_IS_IiEEE St17reference_wrapperIS_IiEE
Integer and floating point (int
, float
) remains the same, integer and floating point wrappers get unwrapped and nested wrappers unwrap one level.
But it doesn't work inside where
:
template <typename container_t, typename predicate_t>
auto where(const container_t &container, predicate_t predicate)
{
auto [b, e] = range(container);
using type = unwrap_t<std::remove_reference_t<decltype(*b)>>;
// ^^^^^^^^ <--- Unwraps iterator's inner type
using reference = std::reference_wrapper<type>;
std::vector<reference> result{};
std::copy_if(b, e, std::back_inserter(result), predicate);
// Debug
std::cout
<< __PRETTY_FUNCTION__ << "\n"
<< '\t' << "decltype(*b) = " << typeid(decltype(*b)).name() << '\n'
<< '\t' << "unwrap *b = " << typeid(unwrap_t<decltype(*b)>).name() << '\n'
<< '\t' << "type = " << typeid(type).name() << '\n'
<< '\t' << "reference = " << typeid(reference).name() << '\n'
<< '\t' << "unwrap type = " << typeid(unwrap_t<type>).name() << '\n';
return result;
}
int main()
{
std::vector v{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
for (const auto &x :
where(where(v, [](auto n){ return n & 0b10; }), [](auto n){ return n & 0b1; })) {
std::cout << &x << ' ';
}
return 0;
}
Debug logs into where
shows that the unwrapper worked on the first call (it did nothing to the type), but not on the second call:
auto where(const container_t&, predicate_t) [with container_t = std::vector<int, std::allocator<int> >; predicate_t = main()::<lambda(auto:1)>] decltype(*b) = i unwrap *b = i type = i reference = St17reference_wrapperIKiE unwrap type = i auto where(const container_t&, predicate_t) [with container_t = std::vector<std::reference_wrapper<const int>, std::allocator<std::reference_wrapper<const int> > >; predicate_t = main()::<lambda(auto:2)>] decltype(*b) = St17reference_wrapperIKiE unwrap *b = St17reference_wrapperIKiE type = St17reference_wrapperIKiE reference = St17reference_wrapperIKS_IKiEE unwrap type = St17reference_wrapperIKiE
On the inner call, input container is std::vector<int>
, so the iterator inner type (decltype(*b)
), the unwrapping (unwrap_t<decltype(*b)>
), tye type (type
) and the unwrapped type (unwrap_t<type>
) are int
, only reference
is std::reference_wrapper
.
On the outer call, input container is std::vector<std::reference_wrapper<const int>>
and all tye types (except reference
) are std::reference_wrapper<const int>
, as if the unwrapper ignored the input type.
What am I doing wrong on my unwrapper? I think the issue might be related to const
propagation.
Code available on Try it online!.
Upvotes: 4
Views: 1867
Reputation: 5085
I think the problem is, that *b
returns a const value (since the container is passed by const reference). Your unwrap
only works on non-const, non-volatile reference_wrapper
. I would go as follows at this problem:
#include <functional>
namespace detail{
template <typename type_t, class orig_t>
struct unwrap_impl
{
using type = orig_t;
};
template <typename type_t, class V>
struct unwrap_impl<std::reference_wrapper<type_t>,V>
{
using type = type_t;
};
}
template<class T>
struct unwrap {
using type = typename detail::unwrap_impl<std::decay_t<T>, T>::type;
};
template <typename type_t>
using unwrap_t = typename unwrap<type_t>::type;
int main() {
static_assert(std::is_same_v<const int&, unwrap_t<const int &>>);
static_assert(std::is_same_v<const int&, unwrap_t<std::reference_wrapper<const int &>>>);
static_assert(std::is_same_v<const int&, unwrap_t<const std::reference_wrapper<const int &>&>>);
}
This should return the original type for anything not a reference_wrapper
and the inner type for cv-qualified reference_wrapper
s and references thereto.
Explanation: I will call the original unwrap
from OP UNWRAP
in the following to differentiate with my version. We want to invoke the reference_wrapper specification of UNWRAP
whenever std::decay_t<T>
is a std::reference_wrapper
. Now this could be simply accomplished if we always invoke UNWRAP
with std::decay_t<T>
instead of T
.
The problem with this is, that if T
is not a reference_wrapper, this will remove all qualifications, i.e. UNWRAP<std::decay_t<const int>>
is int
when we would want it to be const int
.
To get around this we define us template<class type_t, class orig_t> struct unwrap_impl
. We want to always pass this the decayed type for the first argument and the original type (before decaying) as the second argument. Then, we can pass for the general case orig_t
as the result type (as done by using type = orig_t
).
For the specification, we define template<class type_t, class V> struct unwrap_impl<std::reference_wrapper<type_t>, V>
. This will apply whenever type_t
is a reference_wrapper, i.e. when the orignal type is some qualification of a reference_wrapper. We don't care about the second argument (which will be the original type), so we just ignore it. Then we take the inner type of the reference_wrapper as type (using type = type_t;
).
Then we call unwrap_impl
by defining basically template<class type_t> unwrap = detail::unwrap_impl<std::decay_t<type_t>, type_t>;
(this is pseudocode but I think this makes it more clear.
Some examples:
unwrap<int> -> unwrap_impl<int, int> -> int
unwrap<const int> -> unwrap_impl<int, const int> -> const int
unwrap<std::reference_wrapper<const int>> -> unwrap_impl<std::reference_wrapper<const int>, std::reference_wrapper<const int>> -> const int
unwrap<const std::reference_wrapper<const int>> -> unwrap_impl<const std::reference_wrapper<const int>, const std::reference_wrapper<const int>> -> const int
(Again more pseudocode, but I hope its clear)
Edit: fixed some bugs.
Edit2: Seems to work: link
Upvotes: 5