Внутренние определения типов в C ++ - хороший или плохой стиль?

180

Что-то, что я часто делал в последнее время, - это объявление typedef, относящихся к определенному классу внутри этого класса, т.е.

class Lorem
{
    typedef boost::shared_ptr<Lorem> ptr;
    typedef std::vector<Lorem::ptr>  vector;

//
// ...
//
};

Эти типы затем используются в другом месте кода:

Lorem::vector lorems;
Lorem::ptr    lorem( new Lorem() );

lorems.push_back( lorem );

Причины, по которым мне это нравится:

  • Это уменьшает шум, создаваемый шаблонами классов, std::vector<Lorem>становится Lorem::vectorи т. Д.
  • Он служит заявлением о намерениях - в приведенном выше примере класс Lorem предназначен для подсчета ссылок boost::shared_ptrи сохранения в векторе.
  • Это позволяет изменить реализацию - то есть, если Lorem нужно было изменить для навязчивого подсчета ссылок (через boost::intrusive_ptr) на более позднем этапе, то это окажет минимальное влияние на код.
  • Я думаю, что он выглядит «красивее» и, возможно, его легче читать.

Причины, по которым мне это не нравится:

  • Иногда возникают проблемы с зависимостями - если вы хотите встроить, скажем, a Lorem::vectorв другой класс, но вам нужно (или вы хотите) только переслать объявление Lorem (в отличие от введения зависимости от его файла заголовка), тогда вам придется использовать явные типы (например, boost::shared_ptr<Lorem>а не Lorem::ptr), что немного противоречиво.
  • Это может быть не очень распространено и, следовательно, труднее понять?

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

Уилл Бейкер
источник

Ответы:

156

Я считаю, что это отличный стиль, и сам им пользуюсь. Всегда лучше ограничивать область имен, насколько это возможно, и использование классов - лучший способ сделать это в C ++. Например, Стандартная библиотека C ++ интенсивно использует определения типов внутри классов.


источник
Это хороший момент, мне интересно, что это выглядело «красивее», когда мое подсознание деликатно указывало на то, что ограниченный объем - это хорошо . Интересно, однако, делает ли тот факт, что STL использует его преимущественно в шаблонах классов, несколько иное использование? В «конкретном» классе сложнее оправдать?
Уилл Бейкер
1
Стандартная библиотека состоит из шаблонов, а не классов, но я думаю, что обоснование одинаково для обоих.
14

Он служит заявлением о намерениях - в приведенном выше примере класс Lorem предназначен для подсчета ссылок с помощью boost :: shared_ptr и сохранения в векторе.

Это именно то, чего он не делает.

Если я вижу в коде «Foo :: Ptr», я совершенно не понимаю, является ли это shared_ptr или Foo * (в STL есть определения типов :: pointer typedef, которые являются T *, помните) или что-то еще. Esp. если это общий указатель, я вообще не предоставляю typedef, но сохраняю использование shared_ptr явно в коде.

На самом деле, я почти никогда не использую typedef вне метапрограммирования шаблонов.

STL делает такие вещи все время

Дизайн STL с концепциями, определенными в терминах функций-членов и вложенных определений типов, является историческим тупиком, современные библиотеки шаблонов используют бесплатные функции и классы свойств (см. Boost.Graph), поскольку они не исключают встроенные типы из моделирование концепции и потому, что это упрощает адаптацию типов, которые не были разработаны с учетом концепций заданных библиотек шаблонов.

Не используйте STL как повод для тех же ошибок.

Марк Мутц - mmutz
источник
Я согласен с вашей первой частью, но ваше недавнее изменение немного недальновидно. Такие вложенные типы упрощают определение классов признаков, поскольку они обеспечивают разумное значение по умолчанию. Рассмотрим новый std::allocator_traits<Alloc>класс ... вам не нужно специализировать его для каждого отдельного распределителя, который вы пишете, потому что он просто заимствует типы непосредственно из Alloc.
Dennis Zickefoose
@Dennis: В C ++ удобство должно быть на стороне / user / библиотеки, а не ее / author /: пользователь хочет унифицированный интерфейс для признака, и только класс признака может дать это, по причинам, указанным выше). Но даже как Allocавтору специализироваться std::allocator_traits<>на новом типе не сложнее, чем добавить необходимые определения типов. Я также отредактировал ответ, потому что мой полный ответ не вписался в комментарий.
Marc Mutz - mmutz
Но это на стороне пользователя. Как пользователь в allocator_traitsпопытке создать пользовательский аллокатор, я не беспокоить с пятнадцатью членами класса черты ... все , что нужно сделать , это сказать , typedef Blah value_type;и предоставить соответствующие функции - членов, и по умолчанию allocator_traitsбудет выяснить отдых. Далее посмотрите на ваш пример Boost.Graph. Да, он интенсивно использует класс признаков ... но по умолчанию реализация простых graph_traits<G>запросов Gдля собственных внутренних определений типов.
Деннис Зикефуз
1
И даже стандартная библиотека 03 использует классы признаков там, где это уместно ... философия библиотеки заключается не в том, чтобы работать с контейнерами в целом, а в работе с итераторами. Таким образом, он предоставляет iterator_traitsкласс, чтобы ваши общие алгоритмы могли легко запрашивать соответствующую информацию. Что, опять же, по умолчанию запрашивает у итератора собственную информацию. Короче говоря, черты и внутренние определения типов вряд ли являются взаимоисключающими ... они поддерживают друг друга.
Dennis Zickefoose
1
@Dennis: iterator_traitsстало необходимым, потому что он T*должен быть моделью RandomAccessIterator, но вы не можете поместить в него требуемые typedef T*. Как только мы это сделали iterator_traits, вложенные определения типов стали излишними, и я бы хотел, чтобы их тут же удалили. По той же причине (невозможность добавления внутренних определений типов) T[N]не моделирует Sequenceконцепцию STL , и вам понадобятся такие кладжи, как std::array<T,N>. Boost.Range показывает, как можно определить современную концепцию Sequence, которая T[N]может моделировать, поскольку для нее не требуются ни вложенные определения типов, ни функции-члены.
Marc Mutz - mmutz
8

Типы определенно хороший стиль. И все ваши «причины, которые мне нравятся» хороши и правильны.

О проблемах, которые у вас есть с этим. Что ж, предварительное заявление - это не Святой Грааль. Вы можете просто разработать свой код, чтобы избежать многоуровневых зависимостей.

Вы можете переместить typedef за пределы класса, но Class :: ptr намного красивее, чем ClassPtr, что я этого не делаю. Это похоже на пространства имен, как для меня - вещи остаются связанными в рамках.

Иногда я делал

Trait<Loren>::ptr
Trait<Loren>::collection
Trait<Loren>::map

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

Николай Голубев
источник
3

STL постоянно делает такие вещи - определения типов являются частью интерфейса для многих классов в STL.

reference
iterator
size_type
value_type
etc...

все это typedef, которые являются частью интерфейса для различных классов шаблонов STL.

Майкл Берр
источник
Верно, и я подозреваю, что именно здесь я впервые это взял. Кажется, это было бы немного легче оправдать? Я не могу не рассматривать typedef в шаблоне класса как более похожий на переменные, если вы думаете о «метапрограммировании».
Уилл Бейкер
3

Еще одно голосование за то, что это хорошая идея. Я начал это делать, когда писал симуляцию, которая должна была быть эффективной как во времени, так и в пространстве. Все типы значений имели Ptr typedef, который начинался как повышающий общий указатель. Затем я провел некоторое профилирование и изменил некоторые из них на назначаемый указатель Boost без необходимости изменять какой-либо код, в котором использовались эти объекты.

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

KeithB
источник
3

В настоящее время я работаю над кодом, который интенсивно использует такие определения типов. Пока все в порядке.

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

Поэтому перед их использованием следует учесть некоторые моменты.

  • Кому-нибудь еще нужны эти typedef? Часто ли этот класс используется другими классами?
  • Могу ли я сократить использование или скрыть класс? (В случае сокрытия вы также можете подумать об интерфейсах.)
  • Другие люди работают с кодом? Как они это делают? Они подумают, что это проще, или запутаются?
Бодо
источник
1

Я рекомендую переместить эти typedef за пределы класса. Таким образом, вы удаляете прямую зависимость от общих указателей и векторных классов и можете включать их только при необходимости. Если вы не используете эти типы в реализации своего класса, я считаю, что они не должны быть внутренними определениями типов.

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

Кэтэлин Питиш
источник
Это могло бы засорять анонимное пространство имен определениями типов, не так ли ?! Проблема с typedef заключается в том, что он скрывает фактический тип, что может вызвать конфликты при включении в / несколькими модулями, которые трудно найти / исправить. Рекомендуется содержать их в пространствах имен или внутри классов.
Indy9000
3
Конфликты имен и анонимное загрязнение пространства имен имеют мало общего с сохранением имени типа внутри класса или вне его. У вас может возникнуть конфликт имени с вашим классом, но не с вашими определениями типов. Поэтому, чтобы избежать загрязнения имен, используйте пространства имен. Объявите свой класс и связанные с ним определения типов в пространстве имен.
Cătălin Piti
Еще один аргумент в пользу помещения typedef внутри класса - использование шаблонных функций. Когда, скажем, функция получает неизвестный тип контейнера (вектор или список), содержащий неизвестный тип строки (строка или ваш собственный вариант, соответствующий строке). единственный способ выяснить тип полезной нагрузки контейнера - использовать typedef 'value_type', который является частью определения класса контейнера.
Мариус
1

Когда typedef используется только внутри самого класса (т.е. объявлен как закрытый), я думаю, что это хорошая идея. Однако по точно указанным вами причинам я бы не стал использовать его, если typedef необходимо знать за пределами класса. В таком случае рекомендую вынести их за пределы класса.

Стефан Радстрём
источник