Почему примитивные и пользовательские типы действуют по-разному, когда они возвращаются как функция "const" из функции?
#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.
}
Описание проблемы встроено в код. Мой компилятор clang 5.0
.
Мне просто интересно:
Почему С++ обрабатывает встроенные типы и пользовательские типы по-разному в этом случае?
Ответы
Ответ 1
У меня нет цитаты из стандарта, но cppreference подтверждает мои подозрения:
Неклассическое не-массивное значение не может быть cv-квалифицированным. (Примечание: вызов функции или выражение выражений может привести к присвоению класса, не относящегося к классу cv, но cv-квалификатор немедленно удаляется.)
Возвращаемый const int
является просто нормальным int
значением prvalue и делает неконстантную перегрузку лучшим совпадением, чем const
.
Ответ 2
Почему примитивные и пользовательские типы действуют по-разному, когда возвращаются как "const" из функции?
Поскольку часть const
удаляется из примитивных типов, возвращаемых из функций. Вот почему:
В С++ 11 от § 5 Expressions [expr]
(стр. 84):
-
Всякий раз, когда выражение glvalue появляется как операнд оператора, который ожидает значение для этого операнда, значение lvalue-to-rvalue (4.1), переменные-указатели (4.2) или стандартные для преобразования (4.3) значения применяется для преобразования выражения в prvalue. [Примечание: поскольку cv-quali fiers удаляются из типа выражения типа неклассов, когда выражение преобразуется в prvalue, выражение lvalue типа Например, const int может использоваться, когда выражение prvalue типа int требуется. -end note]
И аналогично из § 5.2.3 Explicit type conversion (functional notation) [expr.type.conv]
(стр. 95):
2
Выражение T(), где T - тип простого типа или typename-specifer для типа объекта без массива или (возможно, cv-qualified) тип void, создает приоритет специфицированного тип, который инициализируется значением (8.5; инициализация не выполняется для void()). [Примечание: если T является неклассовым типом, который является cv-qualified, cv-quali fiers игнорируются при определении типа результата prvalue (3.10). -end note]
Это означает, что const int
prvalue, возвращаемый g2()
, эффективно обрабатывается как int
.
Ответ 3
Цитаты из стандарта,
§8/6 Выражения [expr]
Если первоначально значение prvalue имеет тип "cv T", где T является cv-unqualified non-class, non-array type, тип выражения до любого дальнейшего анализа доводится до T.
и §8/9 Выражения [expr]
(акцент мой)
Всякий раз, когда выражение glvalue появляется как операнд оператора который ожидает значение для этого операнда, значение lvalue-to-rvalue, переменные-указатели или стандартные-преобразования указатели-указатели применяется для преобразования выражения в prvalue. [Примечание: Потому что cv-квалификаторы удаляются из типа выражения неклассов введите, когда выражение преобразуется в prvalue, значение lvalue выражение типа const int
может, например, использоваться там, где prvalue требуется выражение типа int
. - end note]
Итак, для g2()
, int
является неклассовым типом и (возвращаемое значение) g2()
является выражение prvalue, тогда const
отбирается, поэтому тип возврата не const int
, а int
. Вот почему вызывается f(T&&)
.
Ответ 4
Предыдущие ответы совершенно верны. Я просто хочу добавить потенциальную мотивацию, почему иногда бывает полезно возвращать объекты const.
В следующем примере class A
дает представление о внутренних данных из class C
, которые в некоторых случаях не могут быть модифицируемыми (Отказ от ответственности, для краткости некоторые существенные части не учитываются), также существуют, вероятно, более простые способы реализации этого поведения ):
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 data
}