Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

#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?

like image 509
xmllmx Avatar asked May 05 '17 06:05

xmllmx


4 Answers

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.

like image 94
Weak to Enuma Elish Avatar answered Oct 21 '22 18:10

Weak to Enuma Elish


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.

like image 24
Pavel P Avatar answered Oct 21 '22 17:10

Pavel P


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.

like image 28
songyuanyao Avatar answered Oct 21 '22 18:10

songyuanyao


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
}
like image 20
chtz Avatar answered Oct 21 '22 18:10

chtz