Здравствуйте, Were, Вы писали:
W>В качестве "защиты от дурака", ради чего и задумывалась эта тема, данный подход не подойдет, так как ничто не мешает написать:
Инициативный дурак что хошь сломает.
Но ты определи — какую сторону защищать собрался.
Если пишешь Function и хочешь избежать вызова её с неверными параметрами — это одно. Кстати, для небольшого наперёд заданного набора функций достаточно сделать ловушки
void Function(Value const&);
template<class T> void Function(T const& x) { Function(up_cast<Value>(x)); }
template<class To, class From>
typename enable_if<is_base_and_derived<To,From>, To const&>::type
up_cast(From const& x) { return x; }
template<class To, class From>
typename enable_if<is_base_and_derived<To,From>, To&>::type
up_cast(From& x) { return x; }
// или даже вообщеtemplate<class T> void Function(T) { STATIC_ASSERT(false); }
А кстати! Можно же сделать вот так:
void Function(ref_t<Value const> v);
// гдеtemplate<class T>
struct ref_t
{
T& value;
ref_t(T& v) : value(v) {}
// методы и операторы для доступа - добавьте по вкусу
}
Здесь мы вводим дополнительную ступень приведения типов, поэтому
Value v = 0; // int ==> Value -> cctor
Function(v); // Value& -> Value const& ==> ref_t<Value>
Function(Value(0)); // Value -> Value const& ==> ref_t<Value>
Function(0); // int ==> Value -> Value const& ==> ref_t<Value>
Знак -> соответствует "бесплатному" приведению (наложение константности, up-cast, и rvalue<->const lvalue), а ==> — платному (любой другой static_cast или подбор шаблона).
Стандарт запрещает совершать более одного платного приведения, поэтому у нас всё хорошо всюду кроме последнего.
Если же пишешь класс и хочешь избежать неверное определение функций — другое.
W>Кроме того особенности использования класса, в котором хочется это применить не позволяют плодить наследников только ради этой самой защиты )
Дык это же миксин. Он нахлобучивается поверх каждого класса в иерархии.
Как, например, в ATL — можно создать иерархию коклассов, а потом завершить каждый кокласс, превратив его в ком-объект.
Может, всё-таки ты объяснишь, какая задача перед тобой стоит?
Я знаю несколько причин, по которым хочется достичь такого поведения:
1) громоздкий тип, хочется автоматический вывод
2) рефакторинг — нужно переделать код с минимумом крови
3) зоны доверия: "для своих" можно пользоваться неявными конструкторами, "на стороне" только явными
Но во всех случаях решения будут разными.
Скажем, для авто-вывода можно ввести промежуточный тип
template<class T>
struct approved_t
{
T value;
/*implicit*/ approved_t(T v) : value(v) {}
};
// для вывода типаtemplate<class T> approved_t<T> approved(T v) { return approved_t<T>(v); }
template<Mega List Of Params>
class YourType
{
public:
/*implicit*/ YourType(approved_t<T> t) : something(t.value) {.....}
explicit YourType(T v) : something(v) {.....}
};
.....
YourType<Your Mega Args> x = 123; // хочется, но нельзя
YourType<Your Mega Args> x = YourType<Your Mega Args>(123); // громоздко
YourType<Your Mega Args> x = approved(123); // можноvoid fun(YourType<Your Mega Args>);
fun(123); // нельзя
fun(YourType<Your Mega Args>(123)); // громоздко
fun(approved(123)); // можно
Понятное дело, я тут не заостряюсь на передаче по значению или по ссылке, это уже второй вопрос.
Есть достаточно надежный способ проверить, соответствует ли код стандарту — проверить компилируемость на компиляторе Comeau.
Твой пример на этом компиляторе не компилится, и я с этим согласен.
Сейчас расскажу почему:
Инициализация копированием предполагает, что в случае несовпадения типов инициализатора и создаваемого объекта, сначала будет создан временный объект с использованием одного из конструкторов инициализируемого типа, а затем этот временный объект используется в прямой инициализации в качестве параметра конструктора копирования инициализируемого типа.
Пример:
class test
{
int i_;
public:
test(int i) : i_(i) {}
};
test t = 1;
В этом примере сначала будет создан временный объект типа test с использованием конструктора test::test(int), а затем этот временный объект будет использован в качетсве аргумента для конструктора test::test(const test&)
Конечно, компилятор может оптимизировать этот процесс, не создавать временный объект и использовать конструктор test::test(int) "напрямую", но стандарт требует, чтобы семантические ограничения все равно принимались во внимание:
12.2/1
...
Even when the creation of the temporary object is avoided (12.8), all the semantic
restrictions must be respected as if the temporary object was created. [Example: even if the copy constructor
is not called, all the semantic restrictions, such as accessibility (clause 11), shall be satisfied. ]
Компиляторы VC (с уверенностью могу сказать про VC6, VC7.1), однако, игнорируют это требование.
Например, если в примере выше объявить конструктор копирования test::test(const test&) закрытым, то VC все равно скомпилирует пример, котя по стандарту не должен.
Мне кажется, что как раз это и является причиной того, что VC компилит вот такой упрощенный пример:
class test
{
int i_;
public:
test(int i) : i_(i) {}
};
class test2
{
public:
test2(const test& t) {}
};
test2 t = 1;
"Правильная" инициализация объекта t должна происходить следующим образом:
создается временный объект test с использованием конструктора test::test(int)
создается временный объект test2 с использованием конструктора test2::test2(const test&)
объект t инициализируется с использованеим конструктора копии.
Как несложно заметить, шаги 1 и 2 — это пользовательские преобразования, а стандарт явно запрещает использование более одного пользовательского преобразования в последовательности преобразований (13.3.3.1.2/1).
VC в данном случае "напрямую" использует конструктор test2::test2(const test& t) без оглядки на конструктор копирования, и поэтому "проскакивает" ограничение на 2 пользовательских преобразования. Но такое поведение не соответствует требованиям стандарта.
По-моему так
Здравствуйте, Were, Вы писали:
W>Как сделать, чтобы явной считалась и инициализация с помощью знака равно(=)? W>Примерно так: W>
W>ExplicitTest TestObj1(0); // OK
W>ExplicitTest TestObj1 = ExplicitTest(0); // OK
W>ExplicitTest *pTestObj = new ExplicitTest(0); // OK
W>ExplicitTest TestObj2 = 0; // Want to be OK
W>void Function( const ExplicitTest &obj);
W>Function( 0 ); // Error
W>
Ну, скажем, так
class Param // то, что мы принимаем и используем
{
protected:
// конструкторы
Param();
explicit Param(int); // здесь explicit для того, чтоб не ошибиться, пребывая в дружественном контексте
.....
public:
// всё остальное, предназначенное для использования
.....
};
class Value : public Param // то, что мы создаём
{
public:
// только конструкторы
Value() {}
/*implicit*/ Value(int x) : Param(x) {}
.....
};
void Function(const Param& obj);
void usage()
{
Value v1(0);
Value v2 = 0;
Function(Value(0));
Function(0); // error
}
Эту идею можно сделать более удобной — скажем, превратив Value в шаблон
template<class Param>
class Value : public Param
{
public:
Value() {}
template<class A1>
/*implicit*/ Value(A1 const& a1) : Param(a1) {}
template<class A1, class A2>
Value(A1 const& a1, A2 const& a2) : Param(a1,a2) {}
// и т.д.
};
class ExplicitTest
{
int m_a;
public:
explicit ExplicitTest( int a ) :
m_a( a ) {}
ExplicitTest( const ExplicitTest &obj ) :
m_a( obj.m_a ) {}
virtual ~ExplicitTest() {}
};
Как сделать, чтобы явной считалась и инициализация с помощью знака равно(=)?
Примерно так:
ExplicitTest TestObj1(0); // OK
ExplicitTest TestObj1 = ExplicitTest(0); // OK
ExplicitTest *pTestObj = new ExplicitTest(0); // OK
ExplicitTest TestObj2 = 0; // Want to be OKvoid Function( const ExplicitTest &obj);
Function( 0 ); // Error
Я конечно понимаю, что там создается временный объект, но компилятор ведь все равно это оптимизирует ) Вообщем хочется запретить создавать неявно временные объекты и разрешить все остальное.
Здравствуйте, Were, Вы писали:
W>Я конечно понимаю, что там создается временный объект, но компилятор ведь все равно это оптимизирует )
Да, имеет право, но класс все равно должен быть copy-constructable
12.2/1
...
even if the copy constructor is not called, all the semantic restrictions, such as accessibility (clause 11), shall be satisfied.
...
W>Вообщем хочется запретить создавать неявно временные объекты и разрешить все остальное.
Насчет инициализации копированием, т.е. записи вида
ExplicitTest TestObj2 = 0;
ничего не выйдет, если конструктор ExplicitTest::ExplicitTest(int) объявлен как explicit.
Что еще из "всего остального" тебе мешает?
Здравствуйте, Bell, Вы писали:
B>ничего не выйдет, если конструктор ExplicitTest::ExplicitTest(int) объявлен как explicit.
Окей, пойдем с другой стороны. Как без объявления explicit конструктора запретить такую форму создания временного объекта?
class ExplicitTest
{
public:
ExplicitTest( int a );
// ...
};
void Function( const ExplicitTest &obj);
Function( 0 ); // Want to be error
B>Что еще из "всего остального" тебе мешает?
Не понял вопроса?
Здравствуйте, Were, Вы писали:
B>>ничего не выйдет, если конструктор ExplicitTest::ExplicitTest(int) объявлен как explicit. W>Окей, пойдем с другой стороны. Как без объявления explicit конструктора запретить такую форму создания временного объекта? W>
W>class ExplicitTest
W>{
W>public:
W> ExplicitTest( int a );
W>// ...
W>};
W>void Function( const ExplicitTest &obj);
W>Function( 0 ); // Want to be error
W>
Я приличного решения не вижу
B>>Что еще из "всего остального" тебе мешает? W>Не понял вопроса?
Вообщем хочется запретить создавать неявно временные объекты и разрешить все остальное.
Один пункт ты указал — желаема запись вида ExplicitTest TestObj2 = 0;. Что еще не устраивает в варианте с explicit-конструктором.
Здравствуйте, Were, Вы писали:
B>>ничего не выйдет, если конструктор ExplicitTest::ExplicitTest(int) объявлен как explicit. W>Окей, пойдем с другой стороны. Как без объявления explicit конструктора запретить такую форму создания временного объекта? W>
W>class ExplicitTest
W>{
W>public:
W> ExplicitTest( int a );
W>// ...
W>};
W>void Function( const ExplicitTest &obj);
W>Function( 0 ); // Want to be error
W>
Как я уже написал выше, это "защита от дурака", так что врядли "дурак" захочет объявлять дополнительные прототипы своей функции )) По большому счету он об этом вообще не знает. А заботиться о том, как пытаются создать объект, должен сам класс.
W>Как я уже написал выше, это "защита от дурака", так что врядли "дурак" захочет объявлять дополнительные прототипы своей функции )) По большому счету он об этом вообще не знает. А заботиться о том, как пытаются создать объект, должен сам класс.
Если "дурак" пишет на C++, ему никакие классы не помогут.
[In theory there is no difference between theory and practice. In
practice there is.]
[Даю очевидные ответы на риторические вопросы]
Здравствуйте, Кодт, Вы писали:
К>Если же пишешь класс и хочешь избежать неверное определение функций — другое.
Я пишу класс. Другой пишет функцию, функция принимает константную ссылку на объект моего класса. Третий вызывает эту функцию. Не хочу, чтобы третий мог необдуманно ее вызвать, указав в качестве параметра что-нибудь отличное от ссылки на явно созданный им объект )
К>Дык это же миксин. Он нахлобучивается поверх каждого класса в иерархии.
Класс вида:
Здравствуйте, Bell, Вы писали:
B>Черт возьми, неужели можно применять подобные вещи в промышленном коде, ради удовольствия писать B>
B>ExplicitTest TestObj2 = 0;
B>
B>
Согласен со смайликом.
Но не всегда "писать" приходится "ради удовольствия".
1) могут быть всякие переходные процессы, связанные с рефакторингом (когда код инициализации ещё хочется трогать, а код вызова уже нужно)
2) нужно эксплицитно надавать по пальцам не повсеместно, а только в особых случаях
— например, разрулить неоднозначность вызова (есть несколько сигнатур Function(), и Function(0) трактуется как Function(Value(0)), вместо ошибки или вызова чего-то иного)
Здравствуйте, Were, Вы писали:
W>Как сделать, чтобы явной считалась и инициализация с помощью знака равно(=)?
Есть контакт!
Нужен еще один конструктор для объекта-ссылки в качестве параметра. Тогда компилятор на этапе инициализации с помощью знака равно будет создавать временный объект-ссылку, а не объект основного класса.
template <typename T>
class ExplicitTest
{
// Объявим в private секции, чтоб явно не юзали )struct ImplicitRef
{
const T &m_a;
ImplicitRef( const T &a ): m_a(a){}
private:
// Запрещаем присваивание, чтоб не ругался на const member
ImplicitRef &operator =( const ImplicitRef & );
};
T m_a;
public:
explicit ExplicitTest( const T &a ) :
m_a( a ) {}
ExplicitTest( const ExplicitTest &obj ) :
m_a( obj.m_a ) {}
// Самое интересное: неявное создание ImplicitRef
// для неявной инициализации ExplicitTest.
ExplicitTest( ImplicitRef ref ) :
m_a( ref.m_a ) {}
virtual ~ExplicitTest() {}
};
Тогда при использовании будет происходить следующее:
Здравствуйте, Were, Вы писали:
W>Здравствуйте, Were, Вы писали:
W>>Что скажете, стандарт оговаривает такое поведение или это фича MSVC? W>Все еще интересно мнение экспертов... )
"Back To The Primitive" (c)Soulfly
Имхо
Ничто не ограничивает полет мысли программиста так, как компилятор.
Здравствуйте, dcb-BanDos, Вы писали:
W>>>Что скажете, стандарт оговаривает такое поведение или это фича MSVC? W>>Все еще интересно мнение экспертов... )
DB>"Back To The Primitive" (c)Soulfly
DB>Имхо
Дело привычки — ничего принципиально плохого я здесь не вижу ) А по вопросу что-нибудь ответить можете? Конкретно интересует нормально-ли то, что компилятор подменяет конструктор копирования на конструктор из ImplicitRef.
И еще не совсем ясна ситуация с тем, что ImplicitRef объявлен в private секции, но при этом он нормально создается для вызова конструктора ExplicitTest.
Здравствуйте, Кодт, Вы писали:
К>Может, всё-таки ты объяснишь, какая задача перед тобой стоит?
Можно назвать это рефакторингом. Класс уже юзается и неявно инициализируется временным объектом (через равно). И это вполне допустимо, и не хочется терять такое поведение. Но я осознал опасность неявного создание класса, из-за его специфики, при инициализации аргументов функции. Теперь хочу запретить такое неявное поведение во избежании дальнейших ошибок.
Здравствуйте, Bell, Вы писали:
B>Компиляторы VC (с уверенностью могу сказать про VC6, VC7.1), однако, игнорируют это требование. B>Например, если в примере выше объявить конструктор копирования test::test(const test&) закрытым, то VC все равно скомпилирует пример, котя по стандарту не должен.
Я так понимаю, неявное создание компилятором объекта приватного класса тоже подпадает под эту особенность?
Здравствуйте, Were, Вы писали:
W>Здравствуйте, Bell, Вы писали:
B>>Компиляторы VC (с уверенностью могу сказать про VC6, VC7.1), однако, игнорируют это требование. B>>Например, если в примере выше объявить конструктор копирования test::test(const test&) закрытым, то VC все равно скомпилирует пример, котя по стандарту не должен.
W>Я так понимаю, неявное создание компилятором объекта приватного класса тоже подпадает под эту особенность?
Конечно.
Здравствуйте, Were, Вы писали:
W>Можно назвать это рефакторингом. Класс уже юзается и неявно инициализируется временным объектом (через равно). И это вполне допустимо, и не хочется терять такое поведение. Но я осознал опасность неявного создание класса, из-за его специфики, при инициализации аргументов функции. Теперь хочу запретить такое неявное поведение во избежании дальнейших ошибок.
Ну, если невозможно навязать пользователям стиль
void function( CRef<YourClass> );
то остаётся путь кровавого рефакторинга: вносишь изменение, гарантированно ломающее компиляцию, и несложным способом подчищаешь все места ошибок.
Два варианта:
1) Объявить конструктор явным, а для удобства — см. фокус с approved.
2) Определить пару классов, один с неявным конструктором — для уже имеющегося кода, а второй с явным — для грядущего. Естественно, с общей реализацией (например, унаследовать один от другого). Заменить все вхождения исходного класса на класс с неявным конструктором. На публику же отдать класс с явным конструктором.