Действительно ли возвращаемая единая инициализированная ссылка действительна

Этот пример кода действителен?

using ref = char&;

ref foo(ref x) {
  return ref{x};
}

int main() {
  char a;
  foo(a);
  return 0;
}

Кажется, что:

  • лязг 3.5 говорит ДА
  • gcc 4.9 говорит НЕТ

    main.cpp: In function 'char& foo(ref)':
    main.cpp:4:15: error: invalid cast of an rvalue expression of type 'char' to type 'ref {aka char&}'
       return ref{x};
                   ^
    

http://coliru.stacked-crooked.com/a/cb6604b81083393f

Так какой компилятор прав? или это не указано?

Это очень легко, поэтому преодолеть ошибку сборки gcc можно:

  1. используя скобки вместо фигурных скобок

    ref foo(ref x) {
      return ref(x);
    }
    
  2. по названию возвращаемого значения

    ref foo(ref x) {
      ref ret{x};
      return ret;
    }
    

вариант 1. нарушает равномерную инициализацию, вариант 2. добавляет бесполезную строку кода.

Подобный вопрос уже задавался здесь: почему я не могу инициализировать ссылку в списке инициализаторов с равномерной инициализацией?

Но упомянутый pr50025 исправлен в gcc 4.9.

Я знаю, что приведенный выше пример кода довольно бесполезен, но я намеренно упростил его, чтобы указать на проблему. В реальной жизни проблемы кода могут быть скрыты в универсальной функции, такой как:

#include <utility>
template <typename Tp, typename... Us>
Tp bar(Us&&... us) {
  return Tp{std::forward<Us>(us)...};
}

1 ответ

Решение

Это кажется упущением в стандарте, где GCC реализует именно то, что требует стандарт, а Clang стремится к тому, что, вероятно, и предполагалось.

Из C++11 (выделено мое):

5.2.3 Явное преобразование типов (функциональная запись) [expr.type.conv]

1 Спецификатор простого типа (7.1.6.2) или спецификатор typename (14.6), за которым следует список выражений в скобках, создает значение указанного типа по заданному списку выражений. Если список выражений является одним выражением, выражение преобразования типа эквивалентно (в определенности и если определено в значении) соответствующему приведенному выражению (5.4). [...]

[...]

3 Аналогично, спецификатор простого типа или спецификатор typename, за которым следует braced-init-list, создает временный объект указанного типа direct-list-initialized (8.5.4) с указанным braced-init-list и его значение этого временного объекта в качестве значения.

В случае с фигурным списком инициализации стандарт не указывает, что это работает так же, как приведение в стиле C. И это не так:

typedef char *cp;
int main() {
  int i;
  (cp(&i)); // okay: C-style casts can be used as reinterpret_cast
  (cp{&i}); // error: no implicit conversion from int * to char *
}

К несчастью, T(expr) эквивалентно (T)expr это также единственное исключение, в котором функциональное приведение не обязательно приводит к prvalue. Стандарт не может определить аналогичное исключение для функционального преобразования, использующего список фигурных скобок init-list для ссылочного типа. В результате в вашем примере ref{x} создает временный тип ref, инициализированный прямым списком из {x}, Этот временный объект затем обрабатывается как значение prvalue, потому что стандарт говорит, что такое поведение должно быть, и это значение prvalue нельзя использовать для привязки к ссылке lvalue.

Я сильно подозреваю, что если бы этот вопрос был вынесен на рассмотрение комитета ISO C++, стандарт был бы изменен, чтобы требовать поведения clang, но, исходя из текущей формулировки стандарта, я думаю, что это GCC, что правильно, по крайней мере для вашего конкретного примера.

Вместо добавления переменной или перехода в скобки, вы можете опустить ref (Tp) чтобы избежать проблемы:

template <typename Tp, typename... Us>
Tp bar(Us&&... us) {
  return {std::forward<Us>(us)...};
}
Другие вопросы по тегам