На RSDN-е угораздило пообщаться с персонажем, насколько уверенном в собственном непогрешимом мнении, что просто караул. К счастью, в Интернетах такие на 100% правые люди встречаются гораздо чаще, чем в реальной жизни. А то работать было бы тяжело.
В общем, когда стало понятно, что цель человека вовсе не в обмене мнениями и не в аргументации своей точки зрения, а в многократном повторении своих убеждений без каких-либо попыток прислушаться к собеседнику, разговор потерял смысл. Но затронутая тема мне показалась заслуживающей того, чтобы развить еще в своем уютненьком.
Итак, речь пойдет о применимости вложенных классов в C++. И, немного, о том, насколько в реальности серьезна проблема невозможности forward declaration для вложенных классов. Все нижеизложенное является моим имхо, сформулированным после небольшого обдумывания данной темы. Наверняка я в чем-то не прав, поэтому все написанное ниже стоит воспринимать просто как мнение одного конкретного старого C++ника, а не истину в последней инстанции. Если же я забыл описать какой-то важный и интересный сценарий использования вложенных классов, то об этом можно (и даже нужно) написать в комментариях.
Начнем с того, что вложенные классы в C++ -- это не что-то особенное, связанное какими-то специфическими узами со своим объемлющим классом (как это было, емнип, в Java). Вложенный класс является обычным классом. Который живет в своем собственном пространстве имен, роль которого играет объемлющий класс. Т.е., по большому счету вот это:
struct Outer { struct Inner { ... }; ... }; |
мало чем отличается от:
namespace Outer { struct Inner { ... }; } |
Тут уместно будет вспомнить, что пространства имен появились в C++ не сразу. Еще более не сразу в C++ компиляторах появилась поддержка пространств имен. Поэтому в конце 1980-х и в 1990-х вложенные классы широко использовались как раз из-за отсутствия пространств имен (или невозможности использовать пространства имен из-за убогости компиляторов). Чем и я сам неоднократно занимался в прошлом.
После того, как поддержка пространств имен появилась повсеместно, актуальность вложенных классов, имхо, сильно снизилась. И ниже я попробую перечислить ситуации, где применение вложенных классов все еще оправдано.
Упрятывание деталей реализации объемлющего класса
Вложенный класс может быть объявлен в объемлющем классе не только как public, но и как private или protected класс:
class DataHolder { // Это private вложенный класс. Который не может быть // просто так использован вне DataHolder-а. class Cache { ... }; Cache cache_; public: ... }; |
В этом случае DataHolder::Cache защищен на уровне компилятора от того, чтобы Cache не использовал кто попало и как попало.
И вот такое ограничение с контролем со стороны компилятора невозможно получить при использовании пространств имен. Т.е. если мы сделаем что-то типа вот такого:
// Это типа отдельный "домик" для DataHolder, в который никто не // должен залезать со своими грязными лапами. namespace data_holder_impl { class Cache { ... }; } class DataHolder { data_holder_impl::Cache cache_; public: ... }; |
То мы никак не можем воспрепятствовать тому, что кто-то решит использовать data_holder_impl::Cache где-то у себя. Ну потому что если нельзя, но очень нужно, то...
Кстати говоря, гарантии со стороны компилятора -- это хорошо, но надобность пожмякать Cache своими похотливыми ручонками может возникнуть гораздо раньше, чем об этом можно было бы подумать. В частности, если Cache нужно будет покрыть какими-то тестами. Если Cache будет private вложенным классом в DataHolder, то нам нужно будет в DataHolder как-то описывать friend-ов, которые смогут использовать Cache. Что, во-первых, неудобно. И, во-вторых, может затруднить написание новых тестов для Cache, особенно если эти тесты пишет не автор Cache и DataHolder. Поэтому и подход с отдельным impl-пространством имен так же имеет право на жизнь, хоть он и не имеет гарантий со стороны компилятора.
Вложенный класс завязан на параметры объемлющего класса
Достаточно распространенный случай в обобщенном программировании:
template< typename A, typename B, std::size_t Capacity > class Outer { using T = typename some_complex_metafunction<A, B>::type; struct Inner { std::array<T, Capacity> data_; ... }; ... }; |
Тут вполне логично иметь Inner вложенным типом для Outer, потому что содержимое Inner напрямую зависит от параметров и содержимого Outer-а.
В принципе, здесь так же можно Inner вынести в отдельное пространство имен, а внутри Outer использовать только using:
namespace outer_impl { template<typename T, std::size_t Capacity> struct Inner { std::array<T, Capacity> data_; ... }; } template< typename A, typename B, std::size_t Capacity > class Outer { using T = typename some_complex_metafunction<A, B>::type; using Inner = outer_impl::Inner<T, Capacity>; ... }; |
Но при таком подходе может оказаться, что придется делать слишком уж большой список параметров для outer_impl::Inner. Что приведет к хрупкому коду, который будет ломаться на раз-два при небольших изменениях в Outer.
Traits типы
Trais типы -- это, как правило, структуры (т.е. struct с только публичными объявлениями внутри), которые содержат описания типов и/или констант, необходимых каким-то другим классам. Вроде такого:
struct ThreadSafeTraits { using LockType = std::mutex; static constexpr bool need_actual_locking = true; ... }; struct ThreadUnsafeTraits { using LockType = NullMutex; static constexpr bool need_actual_locking = false; ... }; template< typename Traits > class EventQueue { using LockType = typename Traits::LockType; LockType lock_; ... }; |
В простых случаях Traits типы содержат разве что using-и и простые константы. Но в более сложных случаях в Traits могут быть свои собственные вложенные типы:
struct TrivialQueueTraits { template< typename T > class Queue { std::queue<T> queue_; ... public: void push(T && v) { queue_.push(std::move(v)); } ... }; }; template< std::size_t Capacity > struct TrivialFixedCapacityQueueTraits { template< typename T > class Queue { std::array<T, Capacity> queue_; ... public: void push(T && v) { ... } ... }; }; template< template Traits > class MessagingSystem { using Queue = typename Traits::Queue; ... }; |
В таких случаях вложенные классы -- это чрезвычайно важная вещь, т.к. параметризовать какой-нибудь условный шаблонный класс MessageSystem типом TrivialQueueTraits мы можем. А вот если бы TrivialQueueTraits был бы пространством имен, то параметризовать MessageSystem этим пространством имен мы бы уже не смогли.
Выше описаны три сценария, в которых я считаю применение вложенных классов в C++ вполне оправданным. И теперь можно немного поговорить о том, почему я оказался в сильном недоумении когда для кого-то отсутствие возможности forward declaration для вложенных типов является настолько серьезной проблемой, что человек готов изливать свою боль в резких выражениях.
Суть в том, что вложенный класс либо должен быть частью реализации объемлющего класса (и, посему, вообще не должен быть виден извне), либо вложенный класс предполагается использовать в шаблонах (и тогда он должен быть полностью определен в месте своего использования).
Т.е. если вложенный класс является private или protected частью объемлющего класса, тогда в его forward declaration нет смысла, т.к. извне класса про него вообще ничего знать не нужно.
А если вложенный класс предназначен для использования в шаблонах, то от его forward declaration мало толку, т.к. ничего полезного с таким объявлением сделать нельзя.
К сожалению, тов.Kluev на RSDN так и не смог привести пример реальной проблемы из-за отсутствия forward declaration. Полагаю, тов.Kluev вынужден активно использовать идиому opaque pointers, которая проникла в C++ из чистого Си. И которая, как мне кажется, мало оправдана в C++. Возможно, так же, тов.Kluev вынужден сопровождать древний C++ный код, который начали писать еще до появления пространств имен. И поэтому люди страдают с вещами типа:
class Storage { public: class Blob { ... }; ... }; |
Тогда как большого смысла в публичном вложенном классе Blob в классе Storage нет, и аналогичного результата можно было бы достичь посредством пространств имен:
namespace storage { class Blob; class Database; }; |
И здесь бы никаких проблем с forward declaration не было бы от слова совсем.
Собственно, вот такое мнение по поводу вложенных классов в C++ мне удалось сформулировать за несколько часов размышлений. Никому его не навязываю. Но, возможно, кому-то оно будет интересно. А может быть даже и полезно.
Если вы видите еще какой-то смысл во вложенных классах, то напишите об этом в комментариях. Я с удовольствием расширю свой кругозор. Как и другие читатели данной заметки.
Комментариев нет:
Отправить комментарий