3

Potentially related articles:

For a STL container C, std::begin(C) and similar access functions including std::data(C) (since C++17) are supposed to have the same behavior of C::begin() and the other corresponding C's methods. However, I am observing some interesting behaviors due to the details of overload resolution involving lvalue/rvalue references and constness.

DataType1 is int* as easily expected. Also, confirmed the by with Boost's type_id_with_cvr. const vector<int> gives int const* No surprise here.

using T = vector<int>;
using DataType1 = decltype(T().data()); // int*
using CT = const T;
using DataType2 = decltype(CT().data()); // int const*

using boost::typeindex::type_id_with_cvr;
cout << type_id_with_cvr<DataType1>() << endl; // prints int*
...

I tried std::data, which can also handle arrays and non-STL containers. But it yields int const*. Similarly, std::begin returns a const iterator, even though T is not const.

using T = vector<int>;
using DataType3 = decltype(std::data(T())); // int const* Why ???
using CT = const T;
using DataType4 = decltype(std::data(CT())); // int const*

Question: What is the reason of this difference? I expected that C.data() and std::data(C) would behave in the same manner.


Some my research: In order to get int* for DataType3, T must be converted to non-const lvalue reference type explicitly. I tried declval, and it was working.

using DataType3 = decltype(std::data(std::declval<T&>())); // int*

std::data provides two overloads:

template <class _Cont> constexpr
auto data(_Cont& __c) -> decltype(__c.data()) { return __c.data(); }

// non-const rvalue reference argument matches to this version.
template <class _Cont> constexpr
auto data(const _Cont& __c) -> decltype(__c.data()) { return __c.data(); }

While resolving overloaded functions for std::data, T(), which is non-const rvalue reference, is matched to the const T& version instead of T& version.

It was not easy to find this specific overload resolution rule in the standard (13.3, over.match). It'd be much clearer if someone could point the exact rules for this issue.

cigien
  • 57,834
  • 11
  • 73
  • 112
minjang
  • 8,860
  • 9
  • 42
  • 61
  • Interesting. For `begin`/`end` this wouldn't normally be noticed, since you would never use them on an rvalue. You'd say `auto && __x = f(); auto it = begin(__x);`, so the argument is always an lvalue. With `data`, one might similarly argue that data without size is useless, so you need an intermediate lvalue, too. – Kerrek SB Dec 09 '15 at 10:29
  • But maybe you should file a library issue anyway. – Kerrek SB Dec 09 '15 at 10:30

2 Answers2

2

This behaviour is attributed to overload resolution rules. As per standard 8.5.3/p5.2 References [dcl.init.ref], rvalue references bind to const lvalue references. In this example:

std::data(T())

You provide to std::data an rvalue. Thus, due to overload resolution rules the overload:

template <class _Cont> constexpr
auto data(const _Cont& __c) -> decltype(__c.data()) { return __c.data(); }

is a better match. Consequently you get const int*

You can't bind a temporary to a non-const lvalue reference.

101010
  • 41,839
  • 11
  • 94
  • 168
  • Thank you. I also figured out for this rule. But I don't quite understand the grounds of this rule. Is there any reason why non-const lvalue reference can't be bound to rvalue? – minjang Dec 09 '15 at 10:38
  • Still a temporary object can be modified by their member functions. I found this fundamental difference between member and free function amusing. (see my answer). Do you know if the standard says something about member functions being able to modify temporaries? – alfC Dec 09 '15 at 11:16
  • This is messed up. `std::data(T())` passes an *rvalue* to `data`, not an rvalue reference. "this would mean that you could alter temporary objects" - well, that's like the whole point of rvalue references, to allow you to steal the guts of temporary objects by altering them. – T.C. Dec 09 '15 at 13:45
  • Thank you so much for finding the very specific rule. – minjang Dec 12 '15 at 19:01
2

The only line that is mildly surprising is using DataType1 = decltype(T().data()); // int*.

...but it is still normal, member functions can be called on temporary objects without being treated as const. This is another non trivial difference between member functions and free functions.

For example, in C++98 (pre-rvalue refs) it was not possible to do std::ofstream("file.txt") << std::string("text") because operator<< is not member and the temporary is treated as const. If operator<< were a member of std::ofstream it would be possible (and may even make sense). (The situation changed later in C++11 with rvalue references but the point is still valid).

Here it is an example:

#include<iostream>
struct A{
    void f() const{std::cout << "const member" << std::endl;}
    void f(){std::cout << "non const member" << std::endl;}
};

void f(A const&){std::cout << "const function" << std::endl;}
void f(A&){std::cout << "non const function" << std::endl;}

int main(){
    A().f(); // prints "non const member"
    f(A()); // prints "const function"
}

The behavior is exposed when the object is temporarily constructed and used. In all other cases I can imagine, member f is equivalent to f free function. (r-value reference qualifications && --for member and function-- can give you a more fine grained control but it was not part of the question.)

alfC
  • 14,261
  • 4
  • 67
  • 118