Можно ли создать константную ссылку на результат тернарного оператора в С++?
В этом коде есть что-то совершенно неочевидное:
float a = 1.;
const float & x = true ? a : 2.; // Note: `2.` is a double
a = 4.;
std::cout << a << ", " << x;
вывод clang и gcc:
4, 1
Можно было бы наивно ожидать, что одно и то же значение будет напечатано дважды, но это не так. Проблема здесь не имеет ничего общего со ссылкой. Существуют некоторые интересные правила, определяющие тип ? :
. Если два аргумента имеют различный тип и могут быть отброшены, они будут использовать временный. Ссылка будет указывать на временный ? :
.
Приведенный выше пример компилируется отлично, и он может или не может выдавать предупреждение при компиляции с -Wall
в зависимости от версии вашего компилятора.
Вот пример того, как легко получить это неправильно в законно выглядящем коде:
template<class Iterator, class T>
const T & min(const Iterator & iter, const T & b)
{
return *iter < b ? *iter : b;
}
int main()
{
// Try to remove the const or convert to vector of floats
const std::vector<double> a(1, 3.0);
const double & result = min(a.begin(), 4.);
cout << &a[0] << ", " << &result;
}
Если ваша логика после этого кода предполагает, что любые изменения на a[0]
будут отражены на result
, это будет неправильно в случаях, когда ?:
создает временную. Кроме того, если в какой-то момент вы делаете указатель на result
, и вы используете его после того, как result
выходит за пределы области видимости, произойдет ошибка сегментации, несмотря на то, что ваш оригинальный a
не вышел из области видимости.
Я чувствую, что есть серьезные причины НЕ использовать эту форму помимо "проблем с ремонтопригодностью и чтением", упомянутых здесь здесь, особенно при написании шаблонного кода, где некоторые из ваших типов и их const'ness может оказаться вне вашего контроля.
Итак, мой вопрос: безопасно ли использовать const &
для тернарных операторов?
P.S. Бонусный пример 1, дополнительные осложнения (см. Также здесь):
float a = 0;
const float b = 0;
const float & x = true ? a : b;
a = 4;
cout << a << ", " << x;
вывод clang:
4, 4
gcc 4.9.3 вывод:
4, 0
С clang этот пример компилируется и работает как ожидалось, но с последними версиями gcc (
P.S.2 Бонусный пример 2, отличный для интервью;):
double a = 3;
const double & a_ref = a;
const double & x = true ? a_ref : 2.;
a = 4.;
std::cout << a << ", " << x;
выход:
4, 3
Ответы
Ответ 1
Прежде всего, результатом условного оператора является либо значение glvalue, обозначающее выбранный операнд, либо значение prvalue, значение которого исходит из выбранного операнда.
Исключение, указанное в T.C.: если хотя бы один операнд имеет тип класса и имеет оператор преобразования в ссылку, результатом может быть lvalue, обозначающий объект, обозначенный возвращаемым значением этого оператора; и если назначенный объект на самом деле является временным, может произойти зависание ссылки. Это проблема с такими операторами, которые предлагают неявное преобразование prvalues в lvalues, а не проблему, введенную самим условным оператором.
В обоих случаях безопасно привязывать ссылку к результату, применяются обычные правила привязки ссылки к lvalue или prvalue. Если ссылка связывается с prvalue (либо результатом prvalue условного, либо значением prvalue, инициализированным из результата lvalue условного выражения), время жизни prvalue увеличивается, чтобы соответствовать времени жизни ссылки.
В исходном случае условное обозначение:
true ? a : 2.
Второй и третий операнды: "lvalue типа float
" и "prvalue типа double
" . Это случай 5 в cppreference summary, результатом которого является "prvalue типа double
" .
Затем ваш код инициализирует ссылку на константу с присвоением другого типа (не связанного с привязкой). Поведение этого заключается в том, чтобы копировать-инициализировать временный объект того же типа, что и ссылка.
В заключение, после const float & x = true ? a : 2.;
, x
является значением l, обозначающим a float
, значение которого является результатом преобразования a
в double
и обратно. (Не уверен, что на моей голове будет сравниваться, равное a
). x
не привязан к a
.
В бонусном случае 1 второй и третий операнды условного оператора "lvalue типа float
" и "lvalue типа const float
". Это случай 3 той же ссылки cppreference,
оба являются значениями одной и той же категории значений и имеют один и тот же тип, за исключением cv-qualification
Поведение состоит в том, что второй операнд преобразуется в "lvalue типа const float
" (обозначая один и тот же объект), а результатом условного выражения является "lvalue типа const float
", обозначающий выделенный объект.
Затем вы привязываете const float &
к "lvalue типа const float
", который связывается напрямую.
Итак, после const float & x = true ? a : b;
, x
напрямую привязан либо к a
, либо к b
.
В бонусе 2, true ? a_ref : 2.
. Второй и третий операнды являются "lvalue типа const double
" и "prvalue типа double
" , поэтому результат "prvalue типа double
" .
Затем вы привязываете это к const double & x
, который является прямым связыванием, поскольку const double
ссылается на double
.
Итак, после const double & x = true ? a_ref : 2.;
, тогда x
является значением l, обозначающим double с тем же значением, что и a_ref
(но x
не привязано к a
).
Ответ 2
Короче: да, это может быть безопасно. Но вам нужно знать, чего ожидать.
Ссылки Lvalue const и ссылки rvalue могут использоваться для продления времени жизни временных переменных (минус исключения, указанные ниже).
Кстати, мы уже узнали из вашего предыдущего вопроса, что gcc 4.9 - это не лучшая ссылка для такого теста. Бонусный пример 1, скомпилированный с gcc 6.1 или 5.3, дает точно такой же результат, как и с clang. Как и предполагалось.
Цитаты из N4140 (выделенные фрагменты):
[class.temporary]
Существует два контекста, в которых временные другая точка, чем конец полного выражения. [...]
Второй контекст - это когда привязка привязана к временному. временный, к которому привязана ссылка, или временное, которое является полный объект подобъекта, к которому привязана ссылка сохраняется на протяжении всей жизни ссылки, за исключением: [нет соответствующих положения к этому вопросу]
[expr.cond]
3) В противном случае, если второй и третий операнды имеют разные типы и либо имеет (возможно, cv-qualified) тип класса, либо если оба являются значениями glvalues той же категории значений и того же типа, за исключением cv-qualification, делается попытка преобразовать каждый из этих операндов к типу другого.
-
Если E2
является lvalue: E1
может быть преобразовано в соответствие с E2
, если E1
может быть неявно преобразовано (раздел 4) в тип "ссылка на lvalue на T2
", при условии ограничения, что при преобразовании ссылка должна привязываться непосредственно к lvalue
-
[...]
-
Если E2
является значением prvalue или если ни одно из приведенных выше преобразований не может быть выполнено, и хотя бы один из операндов имеет (возможно, cv-квалификацию) тип класса:
-
- В противном случае (т.е. если
E1
или E2
имеет тип некласса, или если оба они имеют типы классов, но базовые классы не являются либо тот же или один базовый класс другого): E1
можно преобразовать в соответствие E2
, если E1
можно неявно преобразовать в тип, выражение E2
имел бы, если E2
был преобразован в prvalue (или тип it имеет, если E2
является prvalue)
[...] Если ни один из них не может быть преобразован, операнды остаются неизменными и дальнейшая проверка выполняется, как описано ниже. Если ровно один возможно преобразование, которое применяется к выбранному операнд и преобразованный операнд используются вместо оригинала операнд для остальной части этого раздела.
4) Если второй и третий операнды являются значениями одного и того же значения категории и имеют один и тот же тип, результат такого типа и значения категория [...]
5) В противном случае результатом будет prvalue. Если второй и третий операнды не имеют одного и того же типа и имеют (возможно cv-qualit) тип класса [...]. В противном случае преобразования определены, а преобразованные операнды используются на месте из исходных операндов для остальной части этого раздела.
6) Lvalue-to-rvalue, от массива до указателя и от функции к указателю стандартные преобразования выполняются во втором и третьем операндах. После этих преобразований будет выполнено одно из следующих действий:
- Второй и третий операнды имеют тип арифметики или перечисления; обычные арифметические преобразования выполняются, чтобы привести их к общий тип, и результат этого типа.
Итак, первый пример хорошо определен, чтобы сделать то, что вы испытали:
float a = 1.;
const float & x = true ? a : 2.; // Note: `2.` is a double
a = 4.;
std::cout << a << ", " << x;
x
является ссылкой, привязанной к временному объекту типа float
. Он не ссылается на a
, потому что выражение true ? float : double
определено, чтобы дать double
- и только тогда вы преобразовываете этот double
в новый и другой float
при назначении его x
> .
В вашем втором примере (бонус 1):
float a = 0;
const float b = 0;
const float & x = true ? a : b;
a = 4;
cout << a << ", " << x;
тернарный оператор не должен делать никаких преобразований между a
и b
(кроме соответствия cv-квалификаторов), и он дает lvalue, ссылаясь на const float. x
псевдонимы a
и должны отражать изменения, сделанные в a
.
В третьем примере (бонус 2):
double a = 3;
const double & a_ref = a;
const double & x = true ? a_ref : 2.;
a = 4.;
std::cout << a << ", " << x;
В этом случае E1
можно преобразовать в соответствие с E2
, если E1
может быть неявно преобразовано в тип, который имеет [...] [E2
], если E2
- значение pr20. Теперь, что prvalue имеет то же значение, что и a
, но является другим объектом. x
не псевдоним a
.
Ответ 3
Можно ли создать константную ссылку на результат тройного оператора в С++?
Как Аскер, я бы подвел итоги дискуссии; Это нормально для не-шаблонного кода, на довольно современных компиляторах, с Warnings on. Для шаблонного кода, в качестве обозревателя кода, я бы вообще не поощрял его.