PaperBirdMaster
PaperBirdMaster

Reputation: 13320

std::reference_wrapper unwrap the wrapper

Introduction.

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.

Problem.

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>>>.

What have I tried?.

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.

Question.

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

Answers (1)

n314159
n314159

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_wrappers 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

Related Questions