42
#include <iostream>

using namespace std;

template<typename T>
void f(T&&) { cout << "f(T&&)" << endl; }

template<typename T>
void f(const T&&) { cout << "f(const T&&)" << endl; }

struct A {};
const A g1() { return {}; }
const int g2() { return {}; }

int main()
{
    f(g1()); // outputs "f(const T&&)" as expected.
    f(g2()); // outputs "f(T&&)" not as expected.
}

The issue description is embedded in the code. My compiler is clang 5.0.

I just wonder:

Why does C++ treat built-in types and custom types differently in such a case?

Toby Speight
  • 27,591
  • 48
  • 66
  • 103
xmllmx
  • 39,765
  • 26
  • 162
  • 323
  • there has to be some specific rule in std, but perhaps `const int` return is treated as equivalent to `int`, as there would be no difference. – Pavel P May 05 '17 at 06:49
  • 11
    clang compilation warning `warning: 'const' type qualifier on return type has no effect [-Wignored-qualifiers]` – Paul Rooney May 05 '17 at 07:01
  • 6
    One of those questions which could have been avoided if one built the code with all warnings enabled (``-Wall -Wextra`` on gcc). – Jonas Schäfer May 05 '17 at 07:06
  • 5
    @JonasWielicki: this does not enable all warnings, for backward compatibility reasons, which is why Clang introduced `-Weverything` :) – Matthieu M. May 05 '17 at 08:42
  • @MatthieuM. Today I Learnt. To clarify, is there something equivalent to that with GCC, or does GCC not need it because in GCC -Wall -Wextra in fact turns on all warnings? – Jonas Schäfer May 05 '17 at 13:42
  • 2
    @JonasWielicki: Basically, GCC has no equivalent. Roughly speaking GCC `-Wall` turns on all warnings that existing when it was introduced, but not any new warning for fear that this would prevent people with 3rd party software using `-Wall -Werror` from upgrading GCC (a very valid concern, I should not, seeing as GCC is used to build many Linux distributions). Later on they added `-Wextra` to cover the missing set of warnings, and it was stabilized for the same reasons, and therefore doesn't include warnings included afterwards... – Matthieu M. May 05 '17 at 15:55
  • 2
    @MatthieuM. GCC *does* add to both `-Wall` and `-Wextra` from time to time; the actual policy for `-Wall` is something like "almost certainly indicates a bug, and even if it doesn't, the code can easily be changed to squelch the warning", and for `-Wextra` similarly but with lower estimated odds of indicating a bug. You'll probably have noticed that clang's `-Weverything` includes warnings where there's no workaround if the code is correct as-is - that's what the GCC people want to avoid. – zwol May 05 '17 at 18:25
  • @zwol: Clang `-Weverything` is [meant to be used as a blacklist](http://stackoverflow.com/a/14185534/147192). Clang otherwise manages warnings differently, most notably by enabling the nearly no false positive warnings by default (without any flag) and by having a consistent warning story regardless of optimization levels. I remember designing and implementing the [`-Wdelete-non-virtual-dtor`](https://clang.llvm.org/docs/DiagnosticsReference.html#wdelete-non-virtual-dtor) and the discussions about whether to activate by default or not (hint: it's only enabled by default for abstract classes). – Matthieu M. May 06 '17 at 14:36
  • 1
    @MatthieuM. If I hadn't quit working on GCC in 2005, I would probably have pushed for a more aggressive set of on-by-default warnings by now, and probably also for the adoption of a mode similar to `-Weverything` -- blacklist-style usage was infeasible in GCC as long as there were some warnings _only_ controlled by generic options like `-Wall` or `-pedantic`, but I think that's been fixed by now. – zwol May 06 '17 at 14:53

4 Answers4

30

I don't have a quote from the standard, but cppreference confirms my suspicions:

A non-class non-array prvalue cannot be cv-qualified. (Note: a function call or cast expression may result in a prvalue of non-class cv-qualified type, but the cv-qualifier is immediately stripped out.)

The returned const int is just a normal int prvalue, and makes the non-const overload a better match than the const one.

Weak to Enuma Elish
  • 4,622
  • 3
  • 24
  • 36
  • Good find. I wonder what is the logic behind this, how `const A` return type makes sense, in the first place and if it makes sense, why not `const int`. – alfC May 05 '17 at 07:06
  • 3
    @alfC because `const A` means that all members of `A` cannot be changed (even if you copy A), while there is no members even exist for int. – Pavel P May 05 '17 at 07:08
  • 1
    @alfC [Looks like](http://stackoverflow.com/questions/6299967/what-are-the-use-cases-for-having-a-function-return-by-const-value-for-non-built) it's to stop people from doing weird things with temporary class objects. – Weak to Enuma Elish May 05 '17 at 07:09
  • 3
    That leaves the question why C++ allows class prvalues to be cv-qualified. Imho, that makes just as little sense as allowing it for non-class prvalues. – cmaster - reinstate monica May 05 '17 at 09:12
  • 1
    @cmaster It makes some sense, namely it won't allow you to call non-const member functions on the class prvalue. I don't have an example right now, but I guess one can come up with some scenario, see e.g. http://stackoverflow.com/q/8716330/3093378. Of course in C++11 one shouldn't do this, as it'll make move semantics stop working. – vsoftco May 05 '17 at 10:52
  • 1
    @vsoftco Sorry, I still fail to see the point of allowing cv-qualification on class objects: If I read it correctly, a prvalue is a transient result of an operation with value semantics(!), where the prvalue is owned by the code that invokes the operation. Imho, it's not the business of the operation itself to restrict whether its caller is allowed to modify such a value. After all, it's the caller's business to eventually *destroy* the value, calling its destructor. That is a non-const operation by definition, and it must be allowed to avoid creation of undeletable temporaries. – cmaster - reinstate monica May 05 '17 at 11:27
  • @cmaster Indeed, I'm not advocating at all. But I guess language designers wanted to make sure something like `getA().expensive_operation()` can be prohibited (assuming `expensive_operation` is non-`const`), or similarly for `getA() = B`. Of course now we have [value categories overloads](http://stackoverflow.com/q/21052377/3093378) `&` and `&&` to select which member functions can be invoked on lvalues/rvalues. – vsoftco May 05 '17 at 11:34
22

Why do primitive and user-defined types act differently when returned as 'const' from a function?

Because const part is removed from primitive types returned from functions. Here's why:

In C++11 from § 5 Expressions [expr] (p. 84):

8

Whenever a glvalue expression appears as an operand of an operator that expects a prvalue for that operand, the lvalue-to-rvalue (4.1), array-to-pointer (4.2), or function-to-pointer (4.3) standard conversions are applied to convert the expression to a prvalue. [Note: because cv-qualifiers are removed from the type of an expression of non-class type when the expression is converted to a prvalue, an lvalue expression of type const int can, for example, be used where a prvalue expression of type int is required. —end note]

And similarly from § 5.2.3 Explicit type conversion (functional notation) [expr.type.conv] (p. 95):

2

The expression T(), where T is a simple-type-specifier or typename-specifier for a non-array complete object type or the (possibly cv-qualified) void type, creates a prvalue of the specified type,which is valueinitialized (8.5; no initialization is done for the void() case). [Note: if T is a non-class type that is cv-qualified, the cv-qualifiers are ignored when determining the type of the resulting prvalue (3.10). —end note]

What that means is that const int prvalue returned by g2() is effectively treated as int.

Pavel P
  • 15,789
  • 11
  • 79
  • 128
13

Quotes from the standard,

§8/6 Expressions [expr]

If a prvalue initially has the type “cv T”, where T is a cv-unqualified non-class, non-array type, the type of the expression is adjusted to T prior to any further analysis.

and §8/9 Expressions [expr]

(emphasis mine)

Whenever a glvalue expression appears as an operand of an operator that expects a prvalue for that operand, the lvalue-to-rvalue, array-to-pointer, or function-to-pointer standard conversions are applied to convert the expression to a prvalue. [ Note: Because cv-qualifiers are removed from the type of an expression of non-class type when the expression is converted to a prvalue, an lvalue expression of type const int can, for example, be used where a prvalue expression of type int is required. — end note ]

So for g2(), int is a non-class type, and (the return value of) g2() is a prvalue expression, then const qualifier is removed, so the return type is not const int, but int. That's why f(T&&) is called.

T.C.
  • 133,968
  • 17
  • 288
  • 421
songyuanyao
  • 169,198
  • 16
  • 310
  • 405
2

The previous answers are perfectly valid. I just want to add a potential motivation why it may sometimes be useful to return const objects. In the following example, class A gives a view on internal data from class C, which in some cases shall not be modifyable (Disclaimer, for brevity some essential parts are left out -- also there are likely easier ways to implement this behavior):

class A {
    int *data;
    friend class C; // allow C to call private constructor
    A(int* x) : data(x) {}
    static int* clone(int*) {
        return 0; /* should actually clone data, with reference counting, etc */
    }
public:
    // copy constructor of A clones the data
    A(const A& other) : data(clone(other.data)) {}
    // accessor operators:
    const int& operator[](int idx) const { return data[idx]; }
    // allows modifying data
    int& operator[](int idx) { return data[idx]; }
};

class C {
    int* internal_data;
public:
    C() : internal_data(new int[4]) {} // actually, requires proper implementation of destructor, copy-constructor and operator=
    // Making A const prohibits callers of this method to modify internal data of C:
    const A getData() const { return A(internal_data); }
    // returning a non-const A allows modifying internal data:
    A getData() { return A(internal_data); }
};

int main()
{
    C c1;
    const C c2;

    c1.getData()[0] = 1; // ok, modifies value in c1
    int x = c2.getData()[0]; // ok, reads value from c2
    // c2.getData()[0] = 2;  // fails, tries to modify data from c2
    A a = c2.getData(); // ok, calls copy constructor of A
    a[0] = 2; // ok, works on a copy of c2's data
}
chtz
  • 17,329
  • 4
  • 26
  • 56