В связи с тем, как говорят следователи, "открылись новые обстоятельства дела", то данная тема требует дополнительного обсуждения. Оказывается в новом сттандарте С++ исключена фраза из раздела 12.8 параграфа 7 о том, что "7 A member function template is never instantiated to perform the copy of a class object to an object of its class type."
Теперь конструктор копирования принмает участие а разрешении перегрузки наравне с шаблонными конструкторами и выбирается тот коонструктор, который наиболее подходящий.
Это означает, что поведение компилятора, продемонстрированное в примере из первого сообщения, соответствует новому стандарту С++ 2011.
Но тут возникает другой вопрос. Во всех, повторюсь, во всех книгах по С++ писалось, что если вы хотите предотвратить копирование объектов, то объявите конструктор копирования и копирующий оператор присваивания закрытыми. Конструктивно эта идиома запрета на копирования выражалась следующим образом (возьмем для примера уже приведенный код):
#include "stdafx.h"
#include <iostream>
struct A
{
template <class T>
A( const T & )
{ std::cout << "template <class T> A( const T & )\n"; }
template <class T>
A( T & )
{ std::cout << "template <class T> A( T & )\n"; }
template <class T>
A & operator =( const T &rhs )
{
std::cout << "template <class T> operator =( const T & )\n";
return ( *this );
}
template <class T>
A & operator =( T &rhs )
{
std::cout << "template <class T> operator =( T & )\n";
return ( *this );
}
private:
A( const A & ) { std::cout << "A( const A & )\n"; }
A & operator =( const A & );
};
void f1( A a ) {}
void f2( A a )
{
f1( a );
}
void f3( const A &a )
{
// f1( a );
}
int _tmain(int argc, _TCHAR* argv[])
{
int i = 10;
A a( i );
f2( a );
// f3( a );
return ( 0 ); }
}
В классе
A объявлены закрытыми и конструктор копирования и копирующий оператор присваивания. И, как я указал, эту идиому запрета на копирования объектов одного и того же класса демонстрировали все книги по С++.
Теперь же оказывается, что это совершенно неверная идиома! данный прием не предотвращает копирование объектов. Если запустить этот пример на выполнение, то он успешно скомпилируется и выполнится.
Объявив конструктор копирования и копирующий оператор присваивания, которые принимают константную ссылку на объект, мы всего лишь предотвратили копирование константных объектов. Именно поэтому в этом примере закомментированы строчки в функции
f3 , в теле которой вызывается функция
f1 с константным объектом, а также вызов самой
f3 в теле функции
main, хотя последний можно было бы не комментировать, так как в самой функции
f3 уже нет вызова функции
f1 с константным объектом.
Но при этом, как видно из примера, ничто не мешает вызывать функцию
f2 и из нее функции
f1! Так как для вызовов этих функций не требуется конструктор копирования, принимающий в качестве аргумента константный объект. То есть в этом случае используется тот же самый шаблонный конструктор, который принимает неконстантную ссылку на объект.
А значит идиома запрета на копирование объектов посредством объявления конструктора копирования и копирующего оператора присваивания в том виде, в котором ее демонстрируют во всех книгах по С++, некорректна! Чтобы действительно запретить копирование объектов, надо все конструкторы копирования, которые принимают константную или неконстатную ссылку на объект, объявить закрытыми! То есть, фактически, нужно объявлять закрытыми
четыре конструктора копирования (не говоря уж о
пяти копирующих операторах присваивания)! То есть если хотя бы запретить копирование объектов при создании новых объектов, в нашем классе следовало бы написать
struct A
{
template <class T>
A( const T & )
{ std::cout << "template <class T> A( const T & )\n"; }
template <class T>
A( T & )
{ std::cout << "template <class T> A( T & )\n"; }
template <class T>
A & operator =( const T &rhs )
{
std::cout << "template <class T> operator =( const T & )\n";
return ( *this );
}
template <class T>
A & operator =( T &rhs )
{
std::cout << "template <class T> operator =( T & )\n";
return ( *this );
}
private:
A( const A & ) { std::cout << "A( const A & )\n"; }
A( A & ) { std::cout << "A( A & )\n"; }
A( volatile A & ) { std::cout << "A( volatile A & )\n"; }
A( const volatile A & ) { std::cout << "A( const volatile A & )\n"; }
// не ограничивая общности, оставим без изменения копирующие операторы присваивания
A & operator =( const A & );
};
Получается что то, чему учили во всех книгах по С++, является не совсем корректным. Сделать закрытым лишь один конструктор копирования, принимающий константную ссылку на объект, является совершенно недостаточным для того, чтобы предотвратить копированиие объектов при их создании!
Теперь вернемся к исходному заглавию темы. Я говорил, что компилятор MS VC++ 2010 имеет баг. Но оказалось, что в случае с конструкторами копипрования он ведет себя в соответствии со стандартом. Так есть ли баг в MS VC++ 2010, связанный с копированием объектов? Оказывается есть!

Но только он запрятан не внутри конструкторов копирования, а внутри копирующих операторов присваивания!
Вернемся к примеру, показаннному в начале этого сообщения
#include "stdafx.h"
#include <iostream>
struct A
{
template <class T>
A( const T & )
{ std::cout << "template <class T> A( const T & )\n"; }
template <class T>
A( T & )
{ std::cout << "template <class T> A( T & )\n"; }
template <class T>
A & operator =( const T &rhs )
{
std::cout << "template <class T> operator =( const T & )\n";
return ( *this );
}
template <class T>
A & operator =( T &rhs )
{
std::cout << "template <class T> operator =( T & )\n";
return ( *this );
}
};
void f1( A a ) {}
void f2( A a )
{
f1( a );
}
void f3( const A &a )
{
f1( a );
}
/* It is better that instead of a warning an error message were generated. */
int _tmain(int argc, _TCHAR* argv[])
{
int i = 10;
A a( i );
f2( a );
f3( a );
A a1( i );
const A a2( i );
a1 = a;
a1 = a2;
return ( 0 );
}
Здесь в классе
A объявлены два шаблонных оператора присваивания
template <class T>
A & operator =( const T &rhs )
{
std::cout << "template <class T> operator =( const T & )\n";
return ( *this );
}
template <class T>
A & operator =( T &rhs )
{
std::cout << "template <class T> operator =( T & )\n";
return ( *this );
}
Один из которых принимает неконстантную ссылку на объект, а другой - константную ссылку на объект. По аналогии с конструктором копирования, описанном в стандарте, компилятор создает неявно копирующий оператор присваивания, если он не был явно создан программистом. При этом копирующий оператор присваивания не является шаблонной функцией. Так как в данном примере нешаблонный копирующий оператор присваивания отсутствует, то, значит, компилятор его создал за нас, и он имеет следующее объявление
A & operator =( const A &rhs );
Рассмотрим, что должно произойти при присваивании объектов. В приведенном примере имеется два присваивания
A a1( i );
const A a2( i );
a1 = a;
a1 = a2;
Сначала объекту
a1 присваивается
неконстатный объект
a, затем ему же присваивается
константный объект
a2. В соответствии с разрешением перегрузки, описанной в стандарте, в первом случае должен вызваться шаблонный оператор присваивания, который принимает неконстантную ссылку на объект, а во втором случае - созданный неявно компилятором копирующий оператор приваивания, принимающий констатную ссылку на объект. То есть вывод этой программы должен быть следующим
template <class T> A( T & )
template <class T> A( T & )
template <class T> A( T & )
template <class T> A( T & )
template <class T> A( T & )
template <class T> operator =( T & ) А что выводит код, скомпилированный MS VC++ 2010? Увы, он игнорирует шаблонный оператор присваивания!
template <class T> A( T & )
template <class T> A( T & )
template <class T> A( T & )
template <class T> A( T & )
template <class T> A( T & ) Давайте вместе считать, какие конструкторы и операторы присваивания должны быть вызваны.
Для предложения
A a( i );
должен вызваться шаблонный конструктор копирования, принимающий неконстантную ссылку, то есть мы должны получить сообщение
1) template <class T> A( T & )
Далее вызывается функция
f2 f2( a );
Для инициализации ее параметра должен вызваться тот же шаблонный конструктор копирования
2) template <class T> A( T & )
В теле функции
f2 вызывается функция
f1:
void f2( A a )
{
f1( a );
}
следовательно снова должен вызваться шаблонный конструктор копирования
3) template <class T> A( T & )
Идем дальше. Вызывается функция
f3 f3( a );
Она принимает константную ссылку на объект. Поэтому никакого конструктора вызвано не будет. Но внутри тела функции
f3 вызывается функция
f1.
void f3( const A &a )
{
f1( a );
}
Ей уже передается в качестве аргумента константный объект фуцнкции
f3, поэтому должен вызваться конструктор, который принимает константную ссылку на объект. У нас есть такой шаблонный конструктор и созданный компилятолром неявно конструктор копирования. Согласно стандарту предпочтение будет отдано нешаблонному неявно созданному конструктору копирования, а потому никакого сообщения на экран выведено не будет.
Далее создается объект
a1 A a1( i );
Опять-таки, как и раньще будет вызван шаблонный конструктор. Поэтому на экране появится сообщение
4) template <class T> A( T & )
Затем создается константный объект
a2 const A a2( i );
Так как переменная
i не является константной, то это не имеет значение, что создается константный объект, все равно должен быть вызван шаблонный конструктор, принимающий неконстантную ссылку на объект.
5) template <class T> A( T & )
Эти
пять сообщений компилятор MS VC++ 2010 успешно вывел на экран. А что с предложениями программы
a1 = a;
a1 = a2;
Что здесь будет вызвано? Если следовать положениям нового стандарта, то в первом из этих двух предложений будет вызван шаблонный оператор присваивания, принимающий неконстантную ссылку. То есть должно появиться сообщение
6) template <class T> operator =( T & )
Но этого сообщения нет! Компилятор MS VC++ 2010 на самом деле вопреки ожиданиям вызывает для данного предложения созданный им неявно копирующий оператор, принимающий константную ссылку на объект!
Для второго предложения никакого сообщения не должно появиться на экране, так как в этом случае компилятор поступит правильно, если предпочтение отдаст созданному им неявно копирующему оператору присваивания, так как в правой части предложения с присваиванием стоит константный объект.
Итак, мы видим, что компилятор MS VC++ 2010 некорректно игнорирует шаблонный оператор присваиваничя.
Но это еще не все! Оказывается и другие компиляторы ведут себя непредсказуемо! По крайней мере я пробовал этот пример с помощью
онлайнового компилятора, так его вывод вообще недосчитывает двух вызовов шаблонного конструктора!
template <class T> A( T & )
template <class T> A( T & )
template <class T> A( T & )
template <class T> operator =( T & ) Как мы видим, результат работы компиляторов в этом простом примере непредсказуем! Оба используемых компилятора, MS VC++ 2010 и онлайновый компилятор, очевидно содержат баги.
Может быть я что-то напутал или по невнимательности допустил неточности в коде, или опустил какие-то нюансы стандарта, поэтому если кто-то может поправить меня, то его замечания приветствуются.