Почему и когда троичный оператор возвращает lvalue?

Долгое время я думал, что троичный оператор всегда возвращает значение. Но, к моему удивлению, это не так. В следующем коде я не вижу разницы между возвращаемым значением foo и возвращаемым значением троичного оператора.

#include <iostream>
int g = 20 ;

int foo()
{
    return g ;
}

int main()
{
    int i= 2,j =10 ;

    foo()=10 ; // not Ok 
    ((i < 3) ? i : j) = 7; //Ok
    std::cout << i <<","<<j << "," <<g << std::endl ;
}

Ответ 1

И i и j являются значениями glvalues (подробности см. В этой категории значений).

Тогда, если вы прочитаете эту ссылку на условный оператор, мы подойдем к следующему:

4) Если E2 и E3 являются glvalues одного и того же типа и той же категории значения, то результат имеет тот же тип и категорию значения

Таким образом, результат (i < 3)? i: j (i < 3)? i: j - это glvalue, которому можно присвоить.

Однако делать что-то подобное на самом деле я бы не рекомендовал.

Ответ 2

Правила этого подробно описаны в [expr.cond]. Существует множество веток для нескольких комбинаций типов и категорий значений. Но в конечном счете, выражение является prvalue в случае по умолчанию. Случай в вашем примере охватывается параграфом 5:

Если второй и третий операнды являются glvalues одной и той же категории значений и имеют один и тот же тип, результат имеет тип и категорию значений, и это битовое поле, если второй или третий операнд является битовым полем, или если оба являются битовыми полями.

И i и j, будучи именами переменных, являются lvalue-выражениями типа int. Таким образом, условный оператор создает int lvalue.

Ответ 3

Тернарный условный оператор выдаст lvalue, если тип его второго и третьего операндов - lvalue.

Вы можете использовать шаблон функции is_lvalue (ниже), чтобы выяснить, является ли операнд lvalue, и использовать его в шаблоне функции isTernaryAssignable чтобы узнать, можно ли ему присвоить.

Минимальный пример:

#include <iostream>
#include <type_traits>

template <typename T>
constexpr bool is_lvalue(T&&) {
  return std::is_lvalue_reference<T>{};
}

template <typename T, typename U>
bool isTernaryAssignable(T&& t, U&& u)
{
    return is_lvalue(std::forward<T>(t)) && is_lvalue(std::forward<U>(u));
}

int main(){
    int i= 2,j =10 ;

    ((i < 3) ? i : j) = 7; //Ok

    std::cout << std::boolalpha << isTernaryAssignable(i, j); std::cout << '\n';
    std::cout << std::boolalpha << isTernaryAssignable(i, 10); std::cout << '\n';
    std::cout << std::boolalpha << isTernaryAssignable(2, j); std::cout << '\n';
    std::cout << std::boolalpha << isTernaryAssignable(2, 10); std::cout << '\n';   
}

Выход:

true
false
false
false

LIVE DEMO

Примечание: операнды, которые вы передаете isTernaryAssignable, должны быть такими, чтобы они не подвергались распаду (например, массив, который распадается на указатель).