Неожиданное поведение ссылки на константу

#include <iostream>

class A { 
  public:  
    A(){ cerr << "A Constructor" << endl; }  
    ~A(){ cerr << "A Destructor" << endl; }  
    A(const A &o){ cerr << "A Copy" << endl; } 
    A& operator=(const A &o){ cerr << "A Assignment" << endl; return *this; }
};


class B : public A { 
  public:  
    B() : A() { cerr << "B Constructor" << endl; }  
    ~B(){ cerr << "B Destructor" << endl; }
  private:
    B(const B &o) : A() { cerr << "B Copy" << endl; } 
    B& operator=(const B &o){ cerr << "B Assignment" << endl; return *this; }
};

int main() {  
  A a;  
  const A &b = B();  
  return 0; 
}

В GCC 4.2 я получаю это сообщение:

In function 'int main()':
Line 16: error: 'B::B(const B&)' is private
compilation terminated due to -Wfatal-errors.

Если я удалю «частное» из B, я получу ожидаемый результат:

A Constructor
A Constructor
B Constructor
B Destructor
A Destructor
A Destructor

Мой вопрос: почему создание метода, который не называется закрытым, меняет компилируется ли этот код? Это предусмотрено стандартом? Есть ли обходной путь?


person Zachary Vance    schedule 14.07.2010    source источник
comment
Я не понимаю, почему это не должно компилироваться. FWIW, Комо согласен со мной.   -  person sbi    schedule 14.07.2010
comment
@sbi: Любопытно, что Comeau отклоняет код с отключенными расширениями C++0x, но принимает код с включенными расширениями C++0x.   -  person James McNellis    schedule 14.07.2010
comment
Нет ошибок с g++ 4.4.2, FWIW.   -  person    schedule 14.07.2010
comment
gcc 4.5 принимает код с расширением C++0x и без него.   -  person pmr    schedule 14.07.2010
comment
@James McNellis: в C++03 при инициализации константной ссылки из rvalue реализации разрешено создавать временную ссылку из rvalue и привязывать ссылку к этой временной (можно делать это рекурсивно!) или привязывать непосредственно к значение. В любом случае конструктор копирования должен быть доступен независимо от того, используется он или нет. С++ 0x пересмотрел это, и если rvalue совместимо по ссылке, ссылка должна быть привязана непосредственно к rvalue; временное больше не разрешено, поэтому ограничение на наличие доступного конструктора копирования также было снято.   -  person CB Bailey    schedule 14.07.2010
comment
Это 8.5.3/5 как в C++03, так и в C++0x FCD.   -  person CB Bailey    schedule 14.07.2010
comment
@Чарльз: Да; Я не знал этого, когда разместил этот комментарий; проведя некоторое исследование, я просто опубликовал это в ответе с соответствующими цитатами (ну, я думаю, что это соответствующие цитаты).   -  person James McNellis    schedule 14.07.2010
comment
@James McNellis: Ах, извините, я еще этого не видел.   -  person CB Bailey    schedule 14.07.2010
comment
@pmr: gcc 4.5 принимает код без расширения C++0x? Тогда это оказывается ошибкой.   -  person Zachary Vance    schedule 15.07.2010
comment
@Джеймс Макнеллис, Чарльз Бейли: Спасибо, это ответ на мой вопрос.   -  person Zachary Vance    schedule 15.07.2010


Ответы (3)


Важная формулировка в текущем стандарте (C++03), по-видимому, содержится в §8.5.3, в котором объясняется, как инициализируются ссылки (в этих кавычках T1 — это тип инициализируемой ссылки, а T2 — тип инициализатора). выражение).

Если выражение инициализатора является значением r, где T2 является типом класса, а cv1 T1 совместимо по ссылке с cv2 T2, ссылка привязывается одним из следующих способов (выбор определяется реализацией):

-- Ссылка привязана к объекту, представленному значением r (см. 3.10), или к подобъекту внутри этого объекта.

-- Создается временный объект типа cv1 T2 [sic], и вызывается конструктор для копирования всего объекта rvalue во временный объект. Ссылка привязана к временному объекту или к подобъекту во временном объекте.

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

Таким образом, даже если реализация привязывает ссылку непосредственно к временному объекту, конструктор копирования должен быть доступен.

Обратите внимание, что это изменено в C++0x в соответствии с разрешением дефект CWG 391. Новый язык гласит (N3092 §8.5.3):

В противном случае, если T2 является типом класса и

-- выражение инициализатора является значением r, а cv1 T1 совместимо по ссылке с cv2 T2,

-- T1 не связано со ссылкой на T2, и выражение инициализатора может быть неявно преобразовано в rvalue типа cv3 T3" (это преобразование выбирается путем перечисления применимых функций преобразования (13.3.1.6) и выбора наилучшей из них посредством разрешения перегрузки (13.3). ))

затем ссылка привязывается к выражению инициализатора rvalue в первом случае и к объекту, являющемуся результатом преобразования, во втором случае (или, в любом случае, к соответствующему подобъекту базового класса объекта).

Применяется первый случай, и ссылка привязывается непосредственно к выражению инициализатора.

person James McNellis    schedule 14.07.2010
comment
В примечании к разрешению/стандарте говорится о подобъектах. Очевидно, что в подобъектах происходит полиморфизм, что меня немного смущало. Из любопытства, а стандарт где-то говорит о сужении? Я не могу найти ничего в рабочей копии 2005 года, которую я использую, но я думаю, что просто не знаю, что искать. Я искал в разделе «стандартное преобразование» и раздел о конструкторах копирования. - person Zachary Vance; 15.07.2010
comment
@Zachary: Одной из особенностей стандарта C++ является то, что темы часто распределяются по шести различным разделам, что затрудняет поиск информации в нем, если вы не знаете, где что может быть. В этом случае подпункт об инициализаторах объявления (§8.5) важен, потому что вы инициализируете ссылку. §12.2 о временных объектах важен, потому что у вас есть временный объект типа класса. Часто важной частью является одно короткое предложение в середине абзаца. Иногда я нахожу, что поиск по ключевому слову (например, ссылка) помогает. - person James McNellis; 15.07.2010

Итак, вы используете «инициализацию копирования»:

8.5/11 Инициализаторы

Форма инициализации (с использованием круглых скобок или =) обычно не имеет значения, но имеет значение, когда инициализируемый объект имеет тип класса; увидеть ниже. ...

Инициализация, которая происходит при передаче аргумента, возврате функции, генерации исключения (15.1), обработке исключения (15.3) и списках инициализаторов, заключенных в фигурные скобки (8.5.1), называется инициализацией копированием и эквивалентна форме

T x = a;

Инициализация, которая происходит в новых выражениях (5.3.4), выражениях static_cast (5.2.9), преобразованиях типов функциональной нотации (5.2.3) и инициализаторах базы и члена (12.6.2), называется прямой инициализацией и эквивалентна форма

T x(a);

В 13.3.1.3 «Инициализация конструктором» перегружены для выбранного конструктора:

Когда объекты типа класса инициализируются напрямую (8.5) или инициализируются копированием из выражения того же типа или типа производного класса (8.5), разрешение перегрузки выбирает конструктор. Для прямой инициализации функциями-кандидатами являются все конструкторы класса инициализируемого объекта. Для инициализации копирования функциями-кандидатами являются все конструкторы преобразования (12.3.1) этого класса.

Таким образом, для инициализации копирования должен быть доступен конструктор копирования. Однако компилятору разрешено «оптимизировать» копию:

12.2/1 Временные объекты

Даже когда создание временного объекта избегается (12.8), все семантические ограничения должны соблюдаться, как если бы временный объект был создан. [Пример: даже если конструктор копирования не вызывается, все семантические ограничения, такие как доступность (пункт 11), должны быть удовлетворены. ]

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

 const A &b(B());  

Примечание:

Поскольку более новые версии GCC, по-видимому, имеют другое поведение, я решил опубликовать эту заметку, которая может устранить разницу (при этом оба поведения по-прежнему соответствуют стандартам):

8.5.3/5 Ссылки говорят:

Если выражением инициализатора является rvalue с T2 типом класса, а «cv1 T1» совместим по ссылке с «cv2 T2», ссылка связывается одним из следующих способов (выбор определяется реализацией):

  • Ссылка привязана к объекту, представленному значением r (см. 3.10), или к подобъекту внутри этого объекта.

  • Создается временный объект типа «cv1 T2» [так в оригинале], и вызывается конструктор для копирования всего объекта rvalue во временный объект. Ссылка привязана к временному объекту или к подобъекту во временном объекте.

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

Первоначально я прочитал последнее предложение ("конструктор, который будет использоваться..."), чтобы применить его к обоим параметрам, но, возможно, его следует читать как применимый только к параметру секунд - или, по крайней мере, может быть, так читают сопровождающие GCC. Это.

Я не уверен, что это то, что происходит между разным поведением версий GCC (комментарии приветствуются). Мы определенно достигли предела моих языковых навыков...

person Michael Burr    schedule 14.07.2010
comment
Инициализируемый объект не является объектом, но является ссылкой. - person James McNellis; 14.07.2010
comment
Это тоже была моя первая идея, но это, должно быть, ошибка компилятора, потому что в противном случае более поздние версии gcc вряд ли изменили бы поведение. - person Philipp; 14.07.2010
comment
@Michael Burr: я не понимаю, почему прямая инициализация по сравнению с копией имеет значение для ссылок. 8.5/11 говорит см. ниже; первый пункт 8.5/14 (ниже) говорит: Если целевой тип является ссылочным типом, см. 8.5.3, а 8.5.3 не делает различий между прямой и копирующей инициализацией. - person James McNellis; 14.07.2010
comment
8.5.3/5 обсуждает это и указывает, что для ссылки, привязанной к временному конструктору, который будет использоваться для создания копии, должен вызываться независимо от того, действительно ли копия выполнена. - person Michael Burr; 14.07.2010
comment
@Майкл Берр: Верно. Однако, если мы инициализируем объект, как для прямой, так и для копирующей инициализации требуется доступный конструктор копирования. Почему для прямой инициализации ссылки не требуется доступный конструктор копирования? - person James McNellis; 15.07.2010
comment
@Джеймс и Филипп: я добавил примечание, которое, я думаю, охватывает оба комментария (возможно). - person Michael Burr; 15.07.2010
comment
@Michael: Я тоже прочитал последнее предложение как относящееся к обоим вариантам, но я согласен, что оно немного двусмысленно. Теперь я действительно понятия не имею. :-) Ааа, радость языковых норм. - person James McNellis; 15.07.2010

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

const A& b(B());

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

person Philipp    schedule 14.07.2010