On-line: гостей 0. Всего: 0 [подробнее..]
Программисты всех стран, объединяйтесь!

АвторСообщение



ссылка на сообщение  Отправлено: 28.07.12 19:16. Заголовок: MS VC++ 2010 баг при выборе конструктора когда есть шаблонный конструктор


Основанием для определения этого бага MS VC++ 2010 стал пример фрагмента кода, приведенный на одном из форумов. Я приведу этот фрагмент кода, оформленный мною в виде тестовой программы, которую можно запустить на MS VC++ 2010, чтобы увидеть результаты выполнения.

#include "stdafx.h"
#include <iostream>

struct A 
{
template <class T>
A( T & ) { std::cout << "template <class T> A( T & )\n"; }
};

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, который имеет шаблонный конструктор с одним параметром. И есть три функции, две из которых, f1 и f2 принимают объект класса A по значению, а третья функция f3 принимает константную ссылку на объект класса A.

Какой же должен быть результат работы этой программы?

Сначала обратим внимание, что в соответствии со стандартом С++ 1) конструктором копирования является нешаблонная функция; 2) если у класса явно не объявлен конструктор копирования, то он создается компилятором неявно и имеет тип A( const A & );. Исходя из этого для класса A будет неявно создан уже приведенный конструктор копирования. Итак на самом деле в классе A имеется два конструктора: явно определенный шаблонный конструктор, который принимает ссылку на объект типа T, которым может быть и сам класс A, и созданный компилятором неявно конструктор копирования, который принимает ссылку на константный объект A.

Теперь возникает вопрос, какие конструкторы будут вызваны при передаче объектов данного класса в качестве аргументов вызываемых функций. То есть будет ли вызываться шаблонный конструктор или же созданный неявно конструктор копирования. На это вопрос дает ответ следующее положение стандарта С++ 2011 из раздела 12.8 "Coping and moving class objects". В параграфе № 7 написано: "7 A member function template is never instantiated to perform the copy of a class object to an object of its class type."

Так как во всех случаях вызовов функций из примера копируются объекты одного класса при передаче аргументов параметрам функции, то шаблонный конструктор не должен задействоваться. Он будет вызван только один раз, когда создается переменная a , инициализируемая целовчисленным значением i.
Поэтому результатом работы программы будет вывод на консоль одного сообщения вида

template <class T> A( T & )

А что выводит программа, откомпилированная MS VC++ 10? Она выводит три таких сообщения! первое сообщение соответсвует предложению программы

A a( i );

Здесь все правильно, должен задействоваться конструктор копирования. Но затем начинаются чудеса. Следующие два сообщения относятся к предложению

f2( a );

Сначала вместо неявного конструктора копирования, как это должно быть, вызывается шаблонный конструктор для копирования аргумента a в параметр функции f2. Так как функция f2 в свою очередь вызывает функцию f1, передавая ей также объект класса A, то, опять-таки, вместо неявного конструктора копирования вызывается шаблонный конструктор.
Для функции же f3 и последующего из нее вызова f1 шаблонные конструкторы уже не вызываются, так как параметр функции f3 является константной ссылкой. Так как неявный конструктор копирования имеет в качестве параметра именно константную ссылку, то в этом случае компилятор отдает предпочтение именно ему, а не шаблонному конструктору.

На самом деле это поведение некорректное, так как согласно приведенной цитате из стандарта при копировании объектов одного класса шаблонные конструкторы не используются.

На мой взгляд это очевидный баг компилятора MS VC++ 2010, поэтому я отослал сообщение об ошибке компилятора в Майкрософт. Будет инетерсно, что они ответят.

Спасибо: 0 
ПрофильЦитата Ответить
Ответов - 2 [только новые]





ссылка на сообщение  Отправлено: 28.07.12 19:18. Заголовок: В связи с тем, как г..


В связи с тем, как говорят следователи, "открылись новые обстоятельства дела", то данная тема требует дополнительного обсуждения. Оказывается в новом сттандарте С++ исключена фраза из раздела 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 и онлайновый компилятор, очевидно содержат баги.

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

Спасибо: 0 
ПрофильЦитата Ответить



ссылка на сообщение  Отправлено: 28.07.12 19:19. Заголовок: Решил посмотреть, ка..


Решил посмотреть, как ведут себя компиляторы, которые были выпущены, когда появился стандарт С++ 2003. У меня есть старый компилятор Borland C++ Builder 5.0, и на нем я решил испытать уже обсуждавшийся выше пример

#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 );
}

int main()
{
int i = 10;
A a( i );

f2( a );
f3( a );

A a1( i );
const A a2( i );

a1 = a;
a1 = a2;


return 0;
}


Получил следующий результат

template <class T> A( T & )
template <class T> A( T & )
template <class T> A( T & )
template <class T> operator =( T & )


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

	A a( i ); 
A a1( i );
const A a2( i );


При вызове же функций f2 и f1 никаких соообщений не было, вызывался созданный неявно компилятором конструктор копирования.

Получается забавная ситуация. При копировании объектов с помощью конструктора копирования данный компилятор следовал тому пониманию стандарта С++ 2003, который выражен ранее цитируемой фразой из стандарта "7 A member function template is never instantiated to perform the copy of a class object to an object of its class type.". То есть никакой конкуренции между шаблонным конструктором и неявно созданным конструктором копирования не было. Компилятор однозначано выбирал конструктор копирования, созданный им неявно, даже если копировался неконстантный объект.

У кого есть компилятор Borland C++ Builder 5.0 могут убедиться в этом, вставив в определение класса A следующий конструктор икопирования

	A( const Ф & ) 
{ std::cout << "A( const T & )\n"; }


И тогда вывод наи экран консоли будет следующим:

template <class T> A( T & )
A( const A & )
A( const A & )
A( const A & )
template <class T> A( T & )
template <class T> A( T & )
template <class T> operator =( T & )


В случае же с копирующим оператором присваивания ситуация меняется. Компилятор учитывал шаблонный оператор присваивания при выборе наиболее подходящего и в случае присваивания неконстантного объекта выбирал именно шаблонный оператор присваивания.

Получается, что логика поведения компилятора Borland C++ Builder прямо противоположна логике поведения компилятора MS VC++ 2011.

Как бы найти тот компилятор, который при копировании объектов полностью соответствует новому стандарту С++?!



Спасибо: 0 
ПрофильЦитата Ответить
Ответ:
1 2 3 4 5 6 7 8 9
большой шрифт малый шрифт надстрочный подстрочный заголовок большой заголовок видео с youtube.com картинка из интернета картинка с компьютера ссылка файл с компьютера русская клавиатура транслитератор  цитата  кавычки моноширинный шрифт моноширинный шрифт горизонтальная линия отступ точка LI бегущая строка оффтопик свернутый текст

показывать это сообщение только модераторам
не делать ссылки активными
Имя, пароль:      зарегистрироваться    
Тему читают:
- участник сейчас на форуме
- участник вне форума
Все даты в формате GMT  3 час. Хитов сегодня: 3
Права: смайлы да, картинки да, шрифты да, голосования нет
аватары да, автозамена ссылок вкл, премодерация откл, правка нет