На одном собеседовании поспорил с собеседующим что "друг" обычно первый признок плохого дизайна. На что мне долго пытались втирать, что это хорошо, а в некоторых случаях необходимо. Нопример оператор вывода в поток (С++).
Интересует ваше мнение. Друг зло или нет? Как часто вам приходилось им пользоваться?
Прошу не сносить в холивар, так как вопрос касается C++.
E_>На одном собеседовании поспорил с собеседующим что "друг" обычно первый признок плохого дизайна. На что мне долго пытались втирать, что это хорошо, а в некоторых случаях необходимо. Нопример оператор вывода в поток (С++).
E_>Интересует ваше мнение. Друг зло или нет? Как часто вам приходилось им пользоваться?
Даже у типичного интроверта-программиста могут быть друзья
Мое мнение, что друзья класса — необходимость, иногда вынужденная (например чтобы разрешить создание объектов только из одного конкретного класса). Другое дело, что дружбой часто злоупотребляют там, где достаточно пары публичных функций. Еще одно мое скромное мнение, что вместо дружбы целыми классами лучше было бы иметь возможность объявлять другом функцию-член класса, что-то типа
friend void some_class::some_method();
Не знаю, есть ли такое предложение в C++11/14
Друзья ввода/вывод в поток в принципе не нужны, если есть такое:
Здравствуйте, Europe_cn, Вы писали:
E_>На одном собеседовании поспорил с собеседующим что "друг" обычно первый признок плохого дизайна. На что мне долго пытались втирать, что это хорошо, а в некоторых случаях необходимо. Нопример оператор вывода в поток (С++). E_>Интересует ваше мнение. Друг зло или нет? Как часто вам приходилось им пользоваться? E_>Прошу не сносить в холивар, так как вопрос касается C++.
1. Статическая дружба statefull классов вредна, т.к. нарушает инкапсуляцию.
2. Дружба класса с функцией сомнительна, но иногда упрощает разработку.
3. Но есть один сценарий, когда дружба крайне полезна. Дело в том, что в C++ друг статического родительского типа достаточен, чтобы делать дружественные вызовы полиморфных наследников. Звучит круто, да? =)
Синтетический пример:
class Parent
{
friend class Family;
protected:
virtual void Answer() = 0;
};
class Child : public Parent
{
private:
void Answer() { printf("Child!\n"); }
int encapsulated;
};
class Family
{
public:
void ParentAnswer(Parent *p) { p->Answer(); } /* ok */#if 0
void ChildAnswer(Child *c) { c->Answer(); } /* error! */int breakEncapsulation(Child *c) { return c->encapsulated; } /* error again! */#endif
};
int test(void)
{
Child c;
Family f;
f.ParentAnswer(&c); /* ok, Child->answer will be called */return 0;
}
Обратите внимание: Child статически не дружит с Family и его инкапсуляция не нарушена. Parent -- stateless там нечего нарушать. Но при этом, Family может вызывать закрытые методы класса Child если они полиморфны в его Parent.
Как мне кажется, ради подобных фишек дружба и оставлена в объектной модели C++. Ну ещё можно вспомнить Barton-Nackman trick, но он уже не актуален.
Здравствуйте, Europe_cn, Вы писали:
E_>На одном собеседовании поспорил с собеседующим что "друг" обычно первый признок плохого дизайна. На что мне долго пытались втирать, что это хорошо, а в некоторых случаях необходимо. Нопример оператор вывода в поток (С++).
E_>Интересует ваше мнение. Друг зло или нет? Как часто вам приходилось им пользоваться?
Пример: Контейнер и итераторы для обхода этого контейнера.
Вполне разумно итераторы сделать "друзьями", ибо все вместе они составляют единую систему.
_____________________
С уважением,
Stanislav V. Zudin
PM>>Не знаю, есть ли такое предложение в C++11/14
L_L>да уж что тут не знать, это есть сто лет в обед, поди с первых версий C-фронта было, никаких "высоко-технологичных" C++11 и не надо.
Спасибо, действительно не знал Но не работает для еще не объявленных целиком классов, а именно такой случай мне реально требуется:
class A;
class B
{
friend A::A(); // error C2027: use of undefined type 'A'private:
B() {}
};
class A
{
A() {}
B b;
};
A a;
И не поспоришь, действительно тип A еще не объявлен.
Здравствуйте, Europe_cn, Вы писали:
E_>Прошу не сносить в холивар, так как вопрос касается C++
Да тут и холиварить-то не о чем. Если человек считает использование goto, friend, dynamic_cast, const_cast, reinterpret_cast, c-style cast, макросов нужное подчеркнуть плохим дизайном по определению, потому что прочитал об этом в каком-то там очередном блоге (со ссылкой в лучшем случае на Дейкстру, где тот говорил о Бейсике), значит он дурак по определению.
Здравствуйте, ioj, Вы писали:
I>>Возможно стоит всегда определять друга вместо члена, если не нужна виртуальность. ioj>нужно быть смелее, и переходить сразу к struct matrix с открытыми полями и набору ф-ций для работы с ней
В class matrix обычно vector<Scalar> storage; + информация о размерностях, которая вместе с размером storage составляют инвариант, который нельзя рушить.
Здравствуйте, artem.komisarenko, Вы писали:
AK>Да тут и холиварить-то не о чем. Если человек считает использование goto, friend, dynamic_cast, const_cast, reinterpret_cast, c-style cast, макросов нужное подчеркнуть плохим дизайном по определению, потому что прочитал об этом в каком-то там очередном блоге (со ссылкой в лучшем случае на Дейкстру, где тот говорил о Бейсике), значит он дурак по определению.
+1
Некоторые даже пытаются через свою фанатичную веру в то что какая-то конструкция является плохой по определению, доказать что другие конструкции которые сводятся к этим, которые "плохие по определению", тоже являются плохими.
Недавно на одном из форумов так в очередной раз пытались "доказать" вредность исключений — типа это аналог goto, соответственно исключения — это "плохо":
> throw is a way to put GOTO in your code, the more you see throw or catch , the more your code is full of flaw.
и соответственно мой ответ:
By using your "GOTO" reasoning we can go even further — we can consider harmful even "if" statements. Because at lowest level "if" statement translates into something like:
if(condition) goto label; // jne, brne, etc
So, by using your conclusions: "the more you see if statement, the more your code is full of flaw" — how funny is that?
EP>В class matrix обычно vector<Scalar> storage; + информация о размерностях, которая вместе с размером storage составляют инвариант, который нельзя рушить.
я не хочу скатываться в холивар а-ля anemic vs rich, просто по мне выбор класс + френды для операций вместо свободных ф-ций и простой структуры выглядит несколько странным.
Здравствуйте, ioj, Вы писали:
EP>>В class matrix обычно vector<Scalar> storage; + информация о размерностях, которая вместе с размером storage составляют инвариант, который нельзя рушить. ioj>я не хочу скатываться в холивар а-ля anemic vs rich, просто по мне выбор класс + френды для операций вместо свободных ф-ций и простой структуры выглядит несколько странным.
Почему? friend'ы класса указываются в нём самом — они точно такие же члены его интерфейса (которые имеют доступ к инвариантам) как и member functions — основное отличие в синтаксисе использования.
Причём синтаксис non-member более общий чем member.
Например можно сделать функцию inverse и для double и для matrix:
double x = ...;
Matrix y = .../
auto z = inverse(value);
auto solution = inverse(y) * b;
While we could make a member function to return length, it is better to make it a global friend function. If we do that, we will be able eventually to define the same function to work on built-in arrays and achieve greater uniformity of design. I made size into a member function in STL in an attempt to please the standard committee. I knew that begin, end and size should be global functions but was not willing to risk another fight with the committee. In general, there were many compromises of which I am ashamed. It would have been harder to succeed without making them, but I still get a metallic taste in my mouth when I encounter all the things that I did wrong while knowing full how to do them right.
Там class fvector_int, у которого данные private, и есть
friend std::size_t size(const fvector_int& x)
То что мы даём доступ к инвариантам класса некоторым функциям через friend — не означает, что мы даём его всем.
Здравствуйте, Europe_cn, Вы писали:
E_>Здравствуйте, artem.komisarenko, Вы писали: E_>Сечёшь разницу между "первый признок плохого дизайна" и плохим дизайном по определению есть?
Лучше расскажи нам про разницу между "первый признок плохого дизайна" и "Друг зло или нет?"
E_>Интересует ваше мнение. Друг зло или нет? Как часто вам приходилось им пользоваться?
Здравствуйте, Europe_cn, Вы писали:
E_>Все конструкции были добавлены в язык по объективным причинам, но не все конструкции нужно использовать при первом удобном случае.
А при втором удобном случае можно?
Где граница?
_____________________
С уважением,
Stanislav V. Zudin
Здравствуйте, Stanislav V. Zudin, Вы писали:
SVZ>Здравствуйте, Europe_cn, Вы писали:
E_>>Все конструкции были добавлены в язык по объективным причинам, но не все конструкции нужно использовать при первом удобном случае.
SVZ>А при втором удобном случае можно? SVZ>Где граница?
Границы нет по определению. Главное чтобы аргументация была. Одну и ту же фичу можно реализовать по разному. И стоимость поддержки будет тоже разная.
Весь код тоже можно в main запихнуть.
Здравствуйте, Europe_cn, Вы писали:
E_>>>Все конструкции были добавлены в язык по объективным причинам, но не все конструкции нужно использовать при первом удобном случае.
SVZ>>А при втором удобном случае можно? SVZ>>Где граница?
E_>Границы нет по определению. Главное чтобы аргументация была. Одну и ту же фичу можно реализовать по разному. И стоимость поддержки будет тоже разная. E_>Весь код тоже можно в main запихнуть.
Вот именно.
Качество дизайна можно оценить по стоимости сопровождения этого кода. Но никак не по используемым в нем языковым конструкциям.
Но если ты это понимаешь, зачем вообще завел этот топик? И зачем устроил спор с собеседующим? Спор ради спора? Бесполезная трата времени.
_____________________
С уважением,
Stanislav V. Zudin
EP>То что мы даём доступ к инвариантам класса некоторым функциям через friend — не означает, что мы даём его всем.
я может не совсем понятно выразился. я не ратую за класс с мемберами, я предлагаю использовать структуру с открытыми полями + набор функций, проверка инвариантов пусть будет внутри ф-ций.
Здравствуйте, ioj, Вы писали:
EP>>То что мы даём доступ к инвариантам класса некоторым функциям через friend — не означает, что мы даём его всем. ioj>я может не совсем понятно выразился. я не ратую за класс с мемберами, я предлагаю использовать структуру с открытыми полями + набор функций, проверка инвариантов пусть будет внутри ф-ций.
Ок, возьмём vector_int:
keyword vector_int
{
int *first, *last, *last_allocated;
//...
};
Здравствуйте, ioj, Вы писали:
EP>>Чтобы ты выбрал? struct or class? ioj>class, но у вектора нет операций а-ля mul(matrix, matrix), т.е. синтаксически операции с вектором не порождают уродливые конструкции
А если у вектора был такой size:
friend std::size_t size(const vector_int& x)
то была бы struct?
На счёт матрицы тоже не понятно — у неё ведь тоже есть инвариант, зачем делать его открытым?
EP>то была бы struct?
нет, см. ниже про matrix
EP>На счёт матрицы тоже не понятно — у неё ведь тоже есть инвариант, зачем делать его открытым?
потому что по идее у матрицы кроме конструктора методы не нарушают инвариант
Здравствуйте, ioj, Вы писали:
EP>>На счёт матрицы тоже не понятно — у неё ведь тоже есть инвариант, зачем делать его открытым? ioj>потому что по идее у матрицы кроме конструктора методы не нарушают инвариант
не пойму что ты имеешь ввиду.
Возьмём матрицу выше, допустим сделали её struct с конструктором который устанавливает правильный инвариант.
Можно написать внешний код, который сломает инвариант:
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>Почему бы не запретить это, сделав матрицу классом?
да пусть нарушает, если это делается, то это очень нужно и инвариант в любом случае был бы нарушен, добавлением метода али ещё чего, поэтому если инвариант будет чекаться как предусловие в нужных функциях, то особой проблемы я не вижу.
Здравствуйте, ioj, Вы писали:
EP>>Почему бы не запретить это, сделав матрицу классом? ioj>да пусть нарушает, если это делается, то это очень нужно
1. Если действительно нужно — то скорей всего у матрицы плохой интерфейс, в смысле не хватает чего-то нужного.
2. Я не пойму почему твоя позиция отличается для member и non-member friend — пусть и в случае member'ов всё будет public
ioj>и инвариант в любом случае был бы нарушен, добавлением метода али ещё чего,
мне трудно представить code review который пропустит добавление метода, например, в std::vector
ioj>поэтому если инвариант будет чекаться как предусловие в нужных функциях, то особой проблемы я не вижу.
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>By using your "GOTO" reasoning we can go even further — we can consider harmful even "if" statements. Because at lowest level "if" statement translates into something like:
if(condition) goto label; // jne, brne, etc
До вас пытались донести простую мысль, что throw как и goto (собственно -- в большей степени чем goto, так что throw -- ГОРАЗДО хуже) создаёт нелокальный control-flow. Во что всё это транслируется в ассемблере не так важно (а вы вообще уверены, что ваш if доживёт до ассемблера и не будет куда-нибудь спропагирован по дороге?), а вот разница между локальным и нелокальным потоком управления в программе это очень серьёзно. Потому что ассемблер читает пара упоротых реверсеров, а вот лапшу из goto и исключений, читает и модифицирует каждый второй, и знаете, бывает волосы встают дыбом не только на голове.
Здравствуйте, igna, Вы писали:
I>Так и проверку типов можно отменить. Чтобы не плодить.
Дык мегамодный JS знаешь?
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Здравствуйте, Europe_cn, Вы писали:
E_>На одном собеседовании поспорил с собеседующим что "друг" обычно первый признок плохого дизайна. На что мне долго пытались втирать, что это хорошо, а в некоторых случаях необходимо. Нопример оператор вывода в поток (С++).
E_>Интересует ваше мнение. Друг зло или нет? Как часто вам приходилось им пользоваться?
E_>Прошу не сносить в холивар, так как вопрос касается C++.
Использую для функторов с pointer-to-member доступом к данным.
Например по простому компараторы объявлять, вроде такого: member_order<foo, int, &foo::bar, less<int> >.
Если foo::bar инкапсулировать, так по простому уже не получается, bar доступен только через getter, а getter если в шаблон засовывать, то через указатель на функцию, совсем не эффективно.
Ну и friend в этом случае вполне ok если лямбд нет:
class foo
{
friend class foo_traits;
public:
int bar() const { return bar_; }
private:
int bar_;
};
class foo_traits
{
typedef member_order<foo, int, &foo::bar_> foo_bar_order;
};
Теоретически всё решалось бы публичным объявлением шаблонного компаратора внутре foo, но MSVC в отличе от gcc к сожалению не ест pointer-to-member для незавершенных классов (внутри самого класса).
Здравствуйте, ioj, Вы писали:
ioj>я может не совсем понятно выразился. я не ратую за класс с мемберами, я предлагаю использовать структуру с открытыми полями + набор функций, проверка инвариантов пусть будет внутри ф-ций.
Что в тыоей позиции понятно? -- То, что так, как ты предлагаешь можно написать хорошую, в общем поддерживаемую программу.
А вот что не понтно, так это то, зачем это делать на С++...
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Здравствуйте, sokel, Вы писали:
S>Использую для функторов с pointer-to-member доступом к данным.
Ну это же просто обход дырки в языке/компиляторе, а не систематический подход?
На самом деле сама по себе модель доступа к атрибутам объекта в С++ довольно убогая.
Скажем лично мне часто не хватает возможности предоставить публичный КОНСТАНТНЫЙ доступ.
С другой стороны это дело довольно элементарно обходится, например так:
class ZZZ {
int bar1;
int bar2;
int bar3;
public:
struct Accessor {
const int& bar1;
const int& bar2;
const int& bar3;
Accessor( const ZZZ& theThis ) :
bar1(theThis.bar1 ),
bar2(theThis.bar2 ),
bar3(theThis.bar3 )
{}
};
};
int qqqZZZ( ZZZ& oZzz )
{
ZZZ::Accessor o( oZzz );
// o.bar1 = 5; // не скомпилируется!return o.bar2;
}
только, на мой вкус, надо это очень редко, и можно как-то проще, ну более по-С-шному код зарефакторить в этом месте и всё...
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Здравствуйте, Tilir, Вы писали:
T>До вас пытались донести простую мысль, что throw как и goto (собственно -- в большей степени чем goto, так что throw -- ГОРАЗДО хуже) создаёт нелокальный control-flow.
То есть теперь "доказательство" вредности исключений строится не на том что "goto = зло", exceptions похожи на goto, exceptions = зло, а на том что "non-local-control-flow = зло" ...? То же самое только в профиль
Почему non-local-control-flow считается злом априори? Если целевая задача наиболее красиво и эффективно выражается с помощью нелокальных переходов — то зачем их стеснятся?
Например, со-процедуры зло? А ведь они "мощнее" исключений, так как позволяют прыгать по стэку и вверх и вниз. И да — некоторые задачи отлично выражаются в терминах сопроцедур.
Или другой пример — async/await из последнего C#, которые стали одной из любимых фишек шарпистов. Самый обыкновенный non-local-control-flow. Так что — тоже зло?
T>Во что всё это транслируется в ассемблере не так важно (а вы вообще уверены, что ваш if доживёт до ассемблера и не будет куда-нибудь спропагирован по дороге?),
Что-то может и не дожить — if(true) скорей всего не доживёт, только что это меняет?
T>а вот разница между локальным и нелокальным потоком управления в программе это очень серьёзно. Потому что ассемблер читает пара упоротых реверсеров, а вот лапшу из goto и исключений, читает и модифицирует каждый второй, и знаете, бывает волосы встают дыбом не только на голове.
Волосы могут встать дыбом от кода содержащего любую синтаксическую конструкцию. Код становится плохим не от того что он использует какой-то синтаксис
Здравствуйте, Erop, Вы писали:
E>Здравствуйте, sokel, Вы писали:
S>>Использую для функторов с pointer-to-member доступом к данным.
E>Ну это же просто обход дырки в языке/компиляторе, а не систематический подход?
Кстати, вот ещё вариант когда friend может быть полезен — использование CRTP с открытием деталей реализации для базы:
template<typename impl>
class base
{
public:
void foo() { static_cast<impl&>(*this).bar(); }
};
class impl : public base<impl>
{
friend class base<impl>;
private:
void bar() {}
};
S>Кстати, вот ещё вариант когда friend может быть полезен — использование CRTP с открытием деталей реализации для базы:
Очень часто использую friend именно для этой цели. Например, так:
class CMyDialog : CDialogImpl<CMyDialog>
{
public:
BOOL Show(HWND Parent);
private:
friend class CDialogImpl<CMyDialog>;
BEGIN_MSG_MAP_EX(CMyDialog)
END_MSG_MAP()
};
Здравствуйте, Evgeny.Panasyuk, Вы писали:
E>>С другой стороны это дело довольно элементарно обходится, например так:
class ZZZ {
EP>можно намного проще:
Это ++11, но тут это не суть. Суть в том, что access публикует потроха, "для всех", а через спец. структуру мы публикуем только для тех, кто эту структуру строит => легко найти весь тесно связанный с потрохами код
EP>+ сахар по вкусу.
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Здравствуйте, Erop, Вы писали:
E>>>С другой стороны это дело довольно элементарно обходится, например так:
class ZZZ {
EP>>можно намного проще: E>Это ++11, но тут это не суть.
Кроме in-class initialization, что там C++11?
E>Суть в том, что access публикует потроха, "для всех", а через спец. структуру мы публикуем только для тех, кто эту структуру строит => легко найти весь тесно связанный с потрохами код
Спец структуру/функцию, которая будет давать доступ, можно сделать и в моём случае — я же сказал, "сахар по вкусу". А разница в том, что не надо пробрасывать каждое поле вручную.
Да и .access() — тоже легко найти.
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>Кроме in-class initialization, что там C++11?
Оно самое.
EP>Спец структуру/функцию, которая будет давать доступ, можно сделать и в моём случае — я же сказал, "сахар по вкусу". А разница в том, что не надо пробрасывать каждое поле вручную. EP>Да и .access() — тоже легко найти.
Да, это я тупанул. Не заметил, что ты приватные поля в структуру собрал. Так, конечно, тое можно, если мы только свои поля публикуем.
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Здравствуйте, Erop, Вы писали:
E>Ну это же просто обход дырки в языке/компиляторе, а не систематический подход?
А вот ещё вариант когда реально нужен friend: intrusive_ptr.
Аналогично операторам вывода в поток используется ADL для выбора нужной ф-ции intrusive_ptr_add_ref/release. И если в случае с выводом в поток действительно можно обойтись public функцией типа print, то add_ref/release желательно закрыть.
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>То есть теперь "доказательство" вредности исключений строится не на том что "goto = зло", exceptions похожи на goto, exceptions = зло, а на том что "non-local-control-flow = зло" ...? То же самое только в профиль
Здесь сложно что-то "доказать", речь о стиле и опыте. Человек проще воспринимает локальные конструкции. В своё время именно переход от GOTO к осмысленным IF, WHILE, etc сильно упростил промышленное программирование. Потом придумали threads и exceptions и стало всё как раньше =)
Предпочтение к локальным конструкциям улучшает структурность программы, её поддерживаемость и модифицируемость. Это также облегчает анализ и трансформации в бэкенде компилятора.
EP>Например, со-процедуры зло? А ведь они "мощнее" исключений, так как позволяют прыгать по стэку и вверх и вниз. И да — некоторые задачи отлично выражаются в терминах сопроцедур. Или другой пример — async/await из последнего C#, которые стали одной из любимых фишек шарпистов. Самый обыкновенный non-local-control-flow. Так что — тоже зло?