42

changing a type into a reference to a type, allows one to access the members of the type without creating an instance of the type. This seems to be true for both lvalue references and rvalue references.

declval is implemented with add_rvalue_reference instead of add_lvalue_reference,

  • is this just a convention,
  • or are there examples of use where add_rvalue_reference is preferable?

Edit: I suppose I was slightly vague, these answers are all very good but touch on slightly different points. There are two different answers to use proposed, Howard emphasized that you can choose which reference your type has, making add_rvalue_reference more flexible. The other answers emphasize that the default behavior automatically chooses references which reflect the input type more naturally. I don't know what to pick! If somebody could add two simple examples, motivating the need for each property respectively, then I'll be satisfied.

MrLeeh
  • 5,321
  • 6
  • 33
  • 51
Polymer
  • 1,043
  • 11
  • 13
  • "changing a type into a reference to a type, allows one to access the members of the type without creating an instance of the type." Where did you read that? – KeyC0de Sep 30 '18 at 18:22

4 Answers4

52

With add_rvalue_reference:

  • declval<Foo>() is of type Foo&&.
  • declval<Foo&>() is of type Foo& (reference collapsing: “Foo& &&” collapses to Foo&).
  • declval<Foo&&>() is of type Foo&& (reference collapsing: “Foo&& &&” collapses to Foo&&).

With add_lvalue_reference:

  • declval<Foo>() would be of type Foo&.
  • declval<Foo&>() would be of type Foo& (reference collapsing: “Foo& &” collapses to Foo&).
  • declval<Foo&&>() would be of type Foo& (!) (reference collapsing: “Foo&& &” collapses to Foo&).

that is, you would never get a Foo&&.

Also, the fact that declval<Foo>() is of type Foo&& is fine (you can write Foo&& rr = Foo(); but not Foo& lr = Foo();). And that declval<Foo&&>() would be of type Foo& just feels “wrong”!


Edit: Since you asked for an example:

#include <utility>
using namespace std;

struct A {};
struct B {};
struct C {};

class Foo {
public:
    Foo(int) { } // (not default-constructible)

    A onLvalue()   &  { return A{}; }
    B onRvalue()   && { return B{}; }
    C onWhatever()    { return C{}; }
};

decltype( declval<Foo& >().onLvalue()   ) a;
decltype( declval<Foo&&>().onRvalue()   ) b;
decltype( declval<Foo  >().onWhatever() ) c;

If declval used add_lvalue_reference you couldn't use onRvalue() with it (second decltype).

gx_
  • 4,690
  • 24
  • 31
  • Is there any other reason to distinguish `declval` ( or `declval` ) from `declval` other than the reference matching in member functions, when `Foo` is a user defined type ? – Edward Diener Jun 10 '21 at 19:13
  • If declval<>() returns as rvalue reference of T, then doesn't declval( ) return Foo&&? Why would it call onWhatever()? – Zebrafish Sep 13 '21 at 00:02
14

Yes, the use of add_rvalue_reference gives the client the choice of specifying whether he wants an lvalue or rvalue object of the given type:

#include <type_traits>
#include <typeinfo>
#include <iostream>
#ifndef _MSC_VER
#   include <cxxabi.h>
#endif
#include <memory>
#include <string>
#include <cstdlib>

template <typename T>
std::string
type_name()
{
    typedef typename std::remove_reference<T>::type TR;
    std::unique_ptr<char, void(*)(void*)> own
           (
#ifndef _MSC_VER
                abi::__cxa_demangle(typeid(TR).name(), nullptr,
                                           nullptr, nullptr),
#else
                nullptr,
#endif
                std::free
           );
    std::string r = own != nullptr ? own.get() : typeid(TR).name();
    if (std::is_const<TR>::value)
        r += " const";
    if (std::is_volatile<TR>::value)
        r += " volatile";
    if (std::is_lvalue_reference<T>::value)
        r += "&";
    else if (std::is_rvalue_reference<T>::value)
        r += "&&";
    return r;
}

int
main()
{
    std::cout << type_name<decltype(std::declval<int>())>() << '\n';
    std::cout << type_name<decltype(std::declval<int&>())>() << '\n';
}

Which for me outputs:

int&&
int&
Howard Hinnant
  • 206,506
  • 52
  • 449
  • 577
5

You want to be able to get back aT, a T&, or const/volatile qualified versions thereof. Since may not have a copy or move constructor, you can't just return the type, i.e., a reference needs to be returned. On the other hand, adding an rvalue teference to a reference type has no effect;

 std::declval<T>  -> T&&
 std::declval<T&> -> T&

That is, adding an rvalue reference type has the effect of yielding a result which looks like an object of the passed type!

Dietmar Kühl
  • 150,225
  • 13
  • 225
  • 380
3

An example of where you need control over the returned type can be found in my df.operators library, when you need to provide a noexcept specification. Here's a typical method:

friend T operator+( const T& lhs, const U& rhs )
  noexcept( noexcept( T( lhs ),
                      std::declval< T& >() += rhs,
                      T( std::declval< T& >() ) ) )
{
    T nrv( lhs );
    nrv += rhs;
    return nrv;
}

In generic code, you need to be exact about what you are doing. In the above, T and U are types outside of my control and the noexcept specification for a copy from a const lvalue reference, a non-const lvalue reference and an rvalue reference could be different. I therefore need to be able to express cases like:

  • Can I construct T from a T&? (Use T(std::declval<T&>()))
  • Can I construct T from a const T&? (Use T(std::declval<const T&>()))
  • Can I construct T from a T&&? (Use T(std::declval<T>()))

Luckily, std::declval allows the above by using std::add_rvalue_reference and reference the collapsing rules.

Daniel Frey
  • 55,810
  • 13
  • 122
  • 180