std::reference_wrapper, constructor implementation explaination
Asked Answered
M

1

8

I have been trying to understand the implementation of std::reference_wrapper, from here, which is as follows:

namespace detail {
template <class T> constexpr T& FUN(T& t) noexcept { return t; }
template <class T> void FUN(T&&) = delete;
}
 
template <class T>
class reference_wrapper {
public:
  // types
  typedef T type;
 
  // construct/copy/destroy
  template <class U, class = decltype(
    detail::FUN<T>(std::declval<U>()),
    std::enable_if_t<!std::is_same_v<reference_wrapper, std::remove_cvref_t<U>>>()
  )>
  constexpr reference_wrapper(U&& u) noexcept(noexcept(detail::FUN<T>(std::forward<U>(u))))
    : _ptr(std::addressof(detail::FUN<T>(std::forward<U>(u)))) {}
  reference_wrapper(const reference_wrapper&) noexcept = default;
 
  // assignment
  reference_wrapper& operator=(const reference_wrapper& x) noexcept = default;
 
  // access
  constexpr operator T& () const noexcept { return *_ptr; }
  constexpr T& get() const noexcept { return *_ptr; }
 
  template< class... ArgTypes >
  constexpr std::invoke_result_t<T&, ArgTypes...>
    operator() ( ArgTypes&&... args ) const {
    return std::invoke(get(), std::forward<ArgTypes>(args)...);
  }
 
private:
  T* _ptr;
};

Although the implementation of std::reference_wrapper has been discussed hereand here,but none of it discusses the constructor implementation which i am confused about. My confusions are : 1.) Constructor is a template function , taking a type param (U) different from the template class param T. I have seen member functions of a class being template functions and depending on different type params then the type param of the template class, but i can't think how it is works here. There is a related question here, but i am not able to relate it with my confusion. 2.)I see the second type parameter in the constructor is further used to sfinae out something, but i did not understand howdetail::FUN<T>(std::declval<U>()) is evaluated.

Can someone please explain ?

Edit: This is an example added from microsoft.docs. A snippet of the example is:

    int i = 1;
    std::reference_wrapper<int> rwi(i);  // A.1
    rwi.get() = -1;
    std::cout << "i = " << i << std::endl; //Prints -1

With the implementation of reference_wrapper , and from A.1, how is the constructor of the reference_wrapper called ? Assuming that detail::FUN<T>(std::declval<U>() will be called with detail::FUN<T>(std::declval<int>(), should be a substitution failure because of the deleted overload(Assuming std::declval<int> will be read as an rvalue reference to int). What am i missing here ?

Mould answered 2/1, 2022 at 23:58 Comment(2)
It is a very tricky way of making sure that you cannot create a std::reference_wrapper from a rvalue (e.g. a temporary) without affecting overload resolution in an adverse way. This specification of the constructor is a result of a defect report.Beadruby
"will be called with detail::FUN<T>(std::declval<int>())": No, the constructor uses a forwarding reference, so if you pass the constructor a lvalue (which i is), then it will call detail::FUN<T>(std::declval<int&>()). declval returns a lvalue reference in this case and the non-deleted overload is chosen.Beadruby
S
6

It's a technique you can use when you want the behaviour of the "forwarding reference", U&& in this case, but at the same time restrict what can bind to it.

Deduction of T is aided by the deduction guide provided below. The detail::FUN<T>(std::declval<U>()) is there to ensure that the constructor is disabled when U is deduced to be an rvalue reference, by selecting the deleted overload and producing an invalid expression. It is also disabled if the U is another reference wrapper, in which case the copy constructor should be selected.

Here are a few examples of valid and invalid reference_wrappers:

int i = 1; 

// OK: FUN<int>(std::declval<int&>()) is valid
std::reference_wrapper<int> rwi(i); 

// error: forming pointer to reference
std::reference_wrapper<int&> rwi2(i); 

// OK, uses deduction guide to find T = int
std::reference_wrapper rwi3(i);
std::reference_wrapper rwi4(++i);

// error: cannot deduce T, since there is no deduction guide for
// rvalue reference to T
std::reference_wrapper rwi5(std::move(i));

// error: substitution failure of FUN<int>(int&&)
std::reference_wrapper<int> rwi6(std::move(i));
std::reference_wrapper<int> rwi7(i++);
std::reference_wrapper<int> rwi8(i + i);
std::reference_wrapper<int> rwi9(2);

As you can see, the call to the deleted FUN<T>(T&&) only comes into play in the last 4 cases: when you explicitly specify T, but attempt to construct from an rvalue.

Stannum answered 3/1, 2022 at 0:53 Comment(2)
Thanks. I have added an example in the question to illustrate a use case when an int object is wrapped using reference_wrapper. I understand when you say the overload of detail::fun(). taking an rvalue reference param will be ignored, but would not that mean a case as simple as detail::FUN<T>(std::declval<int>() will be ignored ?Mould
@Mould In that case, the reference_wrapper's type parameter T is explicitly given as int, and U is deduced as int&, which results in the expression detail::FUN<int>(std::declval<int&>()) being valid. I see I made a mistake in my original answer: the reference_wrapper's own parameter T, not its constructor's, is the one being deduced using the deduction guide. I'll fix that and add some examples of valid and invalid uses.Stannum

© 2022 - 2024 — McMap. All rights reserved.