» » » Задействовать для простых тестов наследование, полиморфизм и шаблоны? Почему бы и нет…

 

Задействовать для простых тестов наследование, полиморфизм и шаблоны? Почему бы и нет…

Автор: admin от 13-02-2018, 07:35, посмотрело: 20

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



Добавлю еще, что одним из мощных стимулов к написанию данной статьи стало то, что очень часто на глаза попадаются объемные флеймыобсуждения на тему «ООП не нужно» и, особенно, «шаблоны-дженерики на практике почти никогда не нужны». Мне, как далеко не молодому программисту, начинавшему в 1990-ом как раз с инструментов, в которых не было ни ООП, ни шаблонов-дженериков, странно сталкиваться с подобными точками зрения. Но, чем дальше, тем чаще с ними сталкиваешься. Особенно со стороны приверженцев новых языков программирования, вроде Go или Rust-а.



Сложно сказать, чем это вызвано. Может быть людей перекормили ООП (а это так и было)… Может быть задачи за несколько минувших десятилетий сильно поменялись (а это так и есть)… Может быть и просто «вот и выросло поколение»… Как бы то ни было, можно попробовать на примере из реальной жизни показать, что все не так однозначно ©.



Итак, о чем пойдет речь?

новую фичу под названием deadletter handlers. И эту новую фичу нужно было протестировать. Совсем несложными тестами. В одном тесте нужно было проверить, что программист может установить deadletter handler, в другом — что пользователь может отменить deadletter handler.



Тесты простые, но в них есть одна сложность: нужно было проверять одну и ту же функциональность в разных условиях. Ну, например, deadletter handler может быть повешен на сообщение из разных типов почтовых ящиков. Сообщение, на которое вешается deadletter handler может быть обычным иммутабельным сообщением, может быть обычным мутабельным сообщением, может быть сигналом (поясню: обычные сообщения отличаются от сигналов, поэтому доставка сообщения отличается от доставки сигнала). Сам deadletter handler может быть представлен как указателем на метод класса-агента, так и лямбда-функцией.



А поскольку в SObjectizer код, реализующий deadletter handler-ы, активно использует шаблоны, то для проверки правильности работы шаблонов нужно было в тестах покрыть все разумные сочетания этих условий.



Поясню для тех, кто не очень хорошо знаком с особенностями C++: если, например, шаблонная функция нигде в коде не вызывается, то некоторые компиляторы (вроде старых версий Visual C++) даже не всегда проверяют исходный код этой функции на наличие элементарных ошибок. Поэтому для того, чтобы быть уверенным, что с реализацией шаблона все нормально, нужно, чтобы шаблонные функция или класс были проинстанциированы явно или неявно. Надежнее всего этого достичь через явное использование шаблонной функции/класса в тесте.



Возможное решение «в лоб»



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



Так, для сочетания MPMC-mbox+обычное иммутабельное сообщение+указатель на метод получился бы такой класс:



class mpmc_message_pfn_test_case_t final : public so_5::agent_t {
   state_t st_test{this};
   const so_5::mbox_t mbox_;

   class test_message final : public so_5::message_t {};

public:
   mpmc_message_pfn_test_case_t(context_t ctx)
      :  so_5::agent_t{std::move(ctx)}
      ,  mbox_{so_environment().create_mbox()}
   {}

   virtual void so_define_agent() override {
      this = st_test;
      so_subscribe_deadletter_handler(mbox_,
         &mpmc_message_pfn_test_case_t::on_deadletter);
   }

   virtual void so_evt_start() override {
      so_5::send<test_message>(mbox_);
   }

public:
   void on_deadletter(mhood_t<test_message>) {
      so_deregister_agent_coop_normally();
   }
};


А для сочетания MPMC-mbox+обычное иммутабельное сообщение+лямбда-функция потребовался бы очень похожий класс, но с небольшими изменениями:



class mpmc_message_lambda_test_case_t final : public so_5::agent_t {
   state_t st_test{this};
   const so_5::mbox_t mbox_;

   class test_message final : public so_5::message_t {};

public:
   mpmc_message_lambda_test_case_t(context_t ctx)
      :  so_5::agent_t{std::move(ctx)}
      ,  mbox_{so_environment().create_mbox()}
   {}

   virtual void so_define_agent() override {
      this = st_test;
      so_subscribe_deadletter_handler(mbox_, [this](mhood_t<test_message>) {
            so_deregister_agent_coop_normally();
         });
   }

   virtual void so_evt_start() override {
      so_5::send<test_message>(mbox_);
   }
};


Тогда как для случая direct_mbox+обычное мутабельное сообщение+лямбда функция изменений потребовалось бы больше, но все равно код был бы довольно похожим:



class direct_mutable_message_lambda_test_case_t final : public so_5::agent_t {
   state_t st_test{this};

   class test_message final : public so_5::message_t {};

public:
   direct_mutable_message_lambda_test_case_t(context_t ctx)
      :  so_5::agent_t{std::move(ctx)}
   {}

   virtual void so_define_agent() override {
      this = st_test;
      so_subscribe_deadletter_handler(so_direct_mbox(),
         [this](mutable_mhood_t<test_message>) {
            so_deregister_agent_coop_normally();
         });
   }

   virtual void so_evt_start() override {
      so_5::send<so_5::mutable_msg<test_message(*this);
   }
};


Надеюсь, идея понятна.



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



Другой путь



Поскольку у меня есть жесткий пунктик по поводу копипасты в коде и неспроста, то был выбран другой путь. Через использование шаблонов. И, в одном месте, шаблона шаблонов. К чему затем еще добавилось и наследование с полиморфизмом. Но начнем с простого теста, в котором достаточно только шаблонов.



Простой тест (используются только шаблоны)



Итак, у нас есть три фактора, которые нужно комбинировать между собой. Два из них — тип mbox-а и тип сообщения/сигнала — легко представить в виде параметра шаблона. Не просто с третьим: чем именно реализуется deadletter handler — указателем на функцию или лямбдой. Ну и ладно. Цель же не в том, чтобы избавиться от копипасты совсем. Цель в том, чтобы обойтись самым необходимым ее минимумом.



Поэтому было сделано два шаблонных класса. Первый для случая, когда deadletter handler реализуется указателем на метод:



template< typename Mbox_Case, typename Msg_Type >
class pfn_test_case_t final : public so_5::agent_t
{
   const Mbox_Case m_mbox_holder;

   state_t st_test{ this, "test" };

public:
   pfn_test_case_t( context_t ctx )
      :  so_5::agent_t( std::move(ctx) )
      ,  m_mbox_holder( *self_ptr() )
   {}

   virtual void
   so_define_agent() override
   {
      this = st_test;

      so_subscribe_deadletter_handler( m_mbox_holder.mbox(),
            &pfn_test_case_t::on_deadletter );
   }

   virtual void
   so_evt_start() override
   {
      so_5::send<Msg_Type>( m_mbox_holder.mbox() );
   }

private:
   void
   on_deadletter( mhood_t<Msg_Type> )
   {
      so_deregister_agent_coop_normally();
   }
};


Второй — для случая лямбда функции:



template< typename Mbox_Case, typename Msg_Type >
class lambda_test_case_t final : public so_5::agent_t
{
   const Mbox_Case m_mbox_holder;

   state_t st_test{ this, "test" };

public:
   lambda_test_case_t( context_t ctx )
      :  so_5::agent_t( std::move(ctx) )
      ,  m_mbox_holder( *self_ptr() )
   {}

   virtual void
   so_define_agent() override
   {
      this = st_test;

      so_subscribe_deadletter_handler( m_mbox_holder.mbox(),
            [this](mhood_t<Msg_Type>) {
               so_deregister_agent_coop_normally();
            } );
   }

   virtual void
   so_evt_start() override
   {
      so_5::send<Msg_Type>( m_mbox_holder.mbox() );
   }
};


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



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



class test_message final : public so_5::message_t {};

class test_signal final : public so_5::signal_t {};


Ну и при инстанциировании шаблонов pfn_test_case_t и lambda_test_case_t будут использоваться test_message, so_5::mutable_msg и test_signal. С этим все просто.



А вот с параметром Mbox_Case немного сложнее (хотя, если C++ вы знаете хорошо, то ничего сложного там нет вообще). Этот параметр определяет, какой именно mbox должен использоваться в тестовом случае: MPMC-mbox, который следует создавать специально, или же direct_mbox, который уже есть у каждого агента.



В наших тестах в качестве Mbox_Case используются два очень простых типа:



class direct_mbox_case_t
{
   const so_5::agent_t & m_owner;
public :
   direct_mbox_case_t( const so_5::agent_t & owner )
      :  m_owner(owner)
   {}

   const so_5::mbox_t &
   mbox() const noexcept { return m_owner.so_direct_mbox(); }
};

class mpmc_mbox_case_t
{
   const so_5::mbox_t m_mbox;
public:
   mpmc_mbox_case_t( const so_5::agent_t & owner )
      :  m_mbox( owner.so_environment().create_mbox() )
   {}

   const so_5::mbox_t &
   mbox() const noexcept { return m_mbox; }
};


Экземпляр класса direct_mbox_case_t сохраняет у себя ссылку на агента для того, чтобы в своем методе mbox() возвращать direct_mbox этого агента. А экземпляр класса mpmc_mbox_case_t у себя в конструкторе создает экземпляр MPMC-mbox-а и возвращает ссылку на него в своем методе mbox().



Получается, что когда, например, класс pfn_test_case_t параметризуется direct_mbox_case_t, то в pfn_test_case_t::m_mbox_holder хранится ссылка на сам экземпляр pfn_test_case_t и при вызове m_mbox_holder.mbox() возвращается direct_mbox() самого агента.



А когда pfn_test_case_t параметризуется mpmc_mbox_case_t, то в pfn_test_case_t::m_mbox_holder лежит экземпляр отдельного MPMC-mbox, который создается при конструировании экземпляра pfn_test_case_t.



Ну и тоже самое получается для lambda_test_case_t.



Итак, мы получаем возможность создавать вот такие сочетания для тестовых случаев:



pfn_test_case_t;

pfn_test_case_t
pfn_test_case_t;

pfn_test_case_t;

pfn_test_case_t;

lambda_test_case_t;

lambda_test_case_t
...




Еще по поводу параметра Mbox_Case. У нас используются классы. Хотя можно было параметризовать тестовые классы и функцией, которая бы возвращала mbox_t. Т.е. можно было бы сделать так:



using mbox_maker_t = so_5::mbox_t (*)(const so_5::agent_t &);

so_5::mbox_t mpmc_mbox_maker(const so_5::agent_t & agent) {
   return agent.so_environment().create_mbox();
}

so_5::mbox_t direct_mbox_maker(const so_5::agent_t & agent) {
   return agent.so_direct_mbox();
}

template< mbox_maker_t Mbox_Case, typename Msg_Type >
class pfn_test_case_t final : public so_5::agent_t
{
   const so_5::mbox_t m_mbox;

   state_t st_test{ this, "test" };

public:
   pfn_test_case_t( context_t ctx )
      :  so_5::agent_t( std::move(ctx) )
      ,  m_mbox( Mbox_Case(*self_ptr()) )
   {}
...
};
... pfn_test_case_t<direct_mbox_maker, test_message>;
... lambda_test_case_t<mpmc_mbox_maker, test_signal>;


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



А где же шаблон шаблонов?



А вот шаблон шаблонов, о котором упоминалось выше, потребуется лишь для того, чтобы чуть-чуть упростить процедуру создания тестовых агентов. В общем-то, без него вполне можно было бы обойтись и писать что-то вроде:



env.introduce_coop([](so_5::coop_t coop) {
      coop.make_agent<
         pfn_test_case_t<direct_mbox_case_t, test_message();
   });
env.introduce_coop([](so_5::coop_t coop) {
      coop.make_agent<
         pfn_test_case_t<direct_mbox_case_t, so_5::mutable_msg<test_message();
   });
env.introduce_coop([](so_5::coop_t coop) {
      coop.make_agent<
         pfn_test_case_t<direct_mbox_case_t, test_signal();
   });


Но лучше ввести вспомогательную шаблонную функцию:



template<
   typename Mbox_Case,
   typename Msg_Type,
   template <class, class> class Test_Agent >
void
introduce_test_agent( so_5::environment_t & env )
{
   env.introduce_coop( [&]( so_5::coop_t & coop ) {
         coop.make_agent< Test_Agent<Mbox_Case, Msg_Type> >();
      } );
}


И потом уменьшить себе количество работы и сделать код создания тестов более читаемым:



introduce_test_agent(env);
introduce_test_agent(env);
introduce_test_agent( env );


И вот introduce_test_agent уже и есть тот самый «шаблон шаблонов», т.е. шаблонная функция, одним из шаблонных параметров которой является другой шаблон.



Более сложный тест, в котором потребуется наследование с полиморфизмом



Разобранный выше тест был очень простым, там достаточно было отослать всего одно сообщение, поэтому и тестовые агенты в нем были простыми. А вот следующий тест, проверяющий то, что пользователь может отменить deadletter handler, уже посложнее. Для того, чтобы разобраться с ним для начала посмотрим, как должен работать агент для тестового случая (для простоты берем пока только обычное иммутабельное сообщение test_message):




  • агент должен стартовать, повесить deadletter handler для сообщения test_message;

  • после этого агент должен отослать себе сообщение test_message для проверки того, что deadletter handler действительно есть;

  • когда test_message приходит в deadletter handler, агент должен отменить deadletter handler, после чего он отсылает себе test_message еще раз, а следом специальный сигнал finish;

  • если агент еще раз получает test_message в deadletter handler, значит тест провален;

  • если агент получает только finish без повторного test_message, значит тест успешно пройден и работу агента можно прекращать.



Опять же, чтобы было проще разобраться с последующим кодом, покажем, как бы выглядел агент, написанный «в лоб» для конкретного тестового случая. Самый простой вариант, с direct_mbox-ом и обычным иммутабельным сообщением test_message:



struct finish final : public so_5::signal_t {};

class test_case_specific_agent_t : public so_5::agent_t
{
   state_t st_test{ this };

   int m_deadletters{ 0 };

   void deadletter_handler(mhood_t<test_message>)
   {
      ensure_or_die( 0 == m_deadletters, "m_deadletters must be 0" );
      ++m_deadletters;

      so_unsubscribe_deadletter_handler<test_message>(so_direct_mbox());
      so_5::send<test_message>(*this);
      so_5::send<finish>(*this);
   }

public:
   test_case_specific_agent_t( context_t ctx )
      :  so_5::agent_t(std::move(ctx))
   {}

   virtual void so_define_agent() override
   {
      this = st_test;

      so_subscribe_deadletter_handler(
            so_direct_mbox(), 
            &test_case_specific_agent_t::deadletter_handler);

      st_test.event([this](mhood_t<finish>) {
            so_deregister_agent_coop_normally();
         });
   }

   virtual void so_evt_start() override
   {
      so_5::send<test_message>(*this);
   }
};


Т.е. есть счетчик входов в deadletter_handler (атрибут m_deadletters с нулевым начальным значением). Внутри deadletter_handler() этот счетчик проверяется на ноль и инкрементируется. Если deadletter_handler() будет вызван повторно, то тест провалится.



Метод deadletter_handler отсылает два сообщения. Первое должно быть проигнорировано. Второе должно привести к завершению работы теста (подписка на сигнал finish идет в so_define_agent).



Ну и самый первый экземпляр test_message отсылается в so_evt_start. Т.е. при старте агента.



Однако, это не шаблонный класс. Да еще и заточен под конкретный тестовый сценарий. Как сделать из него шаблон, который можно было бы параметризовать двумя параметрами Mbox_Case и Msg_Type, как в предыдущем простом тесте?



Очевидное решение (не самое интересное)



Очевидное решение было бы в том, чтобы взять классы pfn_test_case_t и lambda_test_case_t из простого теста и просто переделать каждый из них под новую логику работы.



Но это решение нельзя назвать хорошим по очевидной же причине: слишком большой объем дублирующегося кода оказывается в каждом из классов. Соответственно, если допустить ошибку в первой реализации, то она автоматически распространяется на оба класса, но ее устранение требует модификации не одного класса, а обоих. Так же, если потребуется поменять логику поведения, то так же потребуется модифицировать оба класса, а не один. (А логику работы довелось поменять, т.к. изначально использовалась более сложная логика, но в процессе написания статьи выяснилось, что можно сделать заметно проще. Это изменение логики оказалось элементарным и не потребовалось переделывать два независимых друг от друга класса.)



Менее очевидное решение (с наследованием и полиморфизмом)



Итак, нам нужно вынести общие части из pfn_test_case_t и lambda_test_case_t в какой-то общий класс. И т.к. классы агентов в SObjectizer-е должны наследоваться от so_5::agent_t, то скорее всего этот общий класс будет базовым и для pfn_test_case_t, и для lambda_test_case_t.



А вот один ли это будет класс? Давайте посмотрим.



Можно обратить внимание на то, что в демонстрационном test_case_specific_agent_t есть как куски кода, которые зависят от параметров шаблона, так и куски кода, которые от параметров шаблона не зависят. Скажем, наличие состояния st_test и обработчик сигнала finish от параметров шаблона не зависят. А вот установка и отмена deadletter handler-а, отсылка тестового сообщения — зависят.



Это дает нам возможность разбить общий код на две части. Первая часть не будет шаблонной. Для реализации этой части нам потребуется вот такой класс:



class nontemplate_basic_part_t : public so_5::agent_t
{
protected:
   state_t st_test{ this, "test" };

   int m_deadletters{ 0 };

   void
   actual_deadletter_handler()
   {
      ensure_or_die( 0 == m_deadletters, "m_deadletters must be 0" );
      ++m_deadletters;

      do_next_step();
      so_5::send<finish>(*this);
   }

   virtual void
   do_next_step() = 0;

public:
   nontemplate_basic_part_t( context_t ctx )
      :  so_5::agent_t( std::move(ctx) )
   {}

   virtual void
   so_define_agent() override
   {
      this = st_test;

      st_test.event( [this](mhood_t<finish>) {
            so_deregister_agent_coop_normally();
         } );
   }
};


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



Однако, уже в nontemplate_basic_part_t нужно выполнить два действия, которые зависят от Msg_Type — это отмена deadletter handler-а и отсылка еще одного экземпляра Msg_Type. Внутри nontemplate_basic_part_t мы знаем, где и когда эти действия должны быть выполнены, но не можем их выполнить.



Поэтому мы делегируем выполнение этих действий наследнику через чистый виртуальный метод do_next_step(), который и должен быть переопределен в каком-то из классов-наследников.



Наследником же, в котором do_next_step() определяется, будет уже шаблонный класс следующего вида:



template< typename Mbox_Case, typename Msg_Type >
class template_basic_part_t : public nontemplate_basic_part_t
{
protected:
   const Mbox_Case m_mbox_holder;

   virtual void
   do_next_step() override
   {
      so_drop_deadletter_handler< Msg_Type >( m_mbox_holder.mbox() );

      so_5::send< Msg_Type >( *this );
   }

public:
   template_basic_part_t( context_t ctx )
      :  nontemplate_basic_part_t( std::move(ctx) )
      ,  m_mbox_holder( *self_ptr() )
   {}

   virtual void
   so_evt_start() override
   {
      so_5::send<Msg_Type>( m_mbox_holder.mbox() );
   }
};


Тут мы уже видим привычный трюк с атрибутом m_mbox_holder типа Mbox_Case. А так же мы видим реализации виртуальных методов do_next_step (отмена deadletter handler-а и отсылка второго экземпляра Msg_Type) и so_evt_start (отсылка первого экземпляра Msg_Type).



Получается, что nontemplate_basic_part_t и template_basic_part_t уже содержат 95% нужной тестовому агенту функциональности. Осталось всего ничего — сделать pfn_test_case_t и lambda_test_case_t в которых бы устанавливался deadletter handler нужного вида.



Вот так это будет выглядеть:



template< typename Mbox_Case, typename Msg_Type >
class pfn_test_case_t final : public template_basic_part_t< Mbox_Case, Msg_Type >
{
   using base_type_t = template_basic_part_t< Mbox_Case, Msg_Type >;

public:
   using base_type_t::base_type_t;

   virtual void
   so_define_agent() override
   {
      base_type_t::so_define_agent();

      thisso_subscribe_deadletter_handler( thism_mbox_holder.mbox(),
            &pfn_test_case_t::on_deadletter );
   }  

private:
   void
   on_deadletter( so_5::mhood_t<Msg_Type> )
   {
      thisactual_deadletter_handler();
   }
};

template< typename Mbox_Case, typename Msg_Type >
class lambda_test_case_t final : public template_basic_part_t< Mbox_Case, Msg_Type >
{
   using base_type_t = template_basic_part_t< Mbox_Case, Msg_Type >;

public:
   using base_type_t::base_type_t;

   virtual void
   so_define_agent() override
   {
      base_type_t::so_define_agent();

      thisso_subscribe_deadletter_handler( thism_mbox_holder.mbox(),
            [this](so_5::mhood_t<Msg_Type>) {
               thisactual_deadletter_handler();
            } );
   }
};


Тут просто классическое наследование с перекрытием виртуального метода предка для того, чтобы расширить его поведение: в so_define_agent() сперва вызывается so_define_agent() из базового класса, после чего устанавливается deadletter handler должного вида.



Вот в итоге и получается старое доброе ООП, с наследованием (реализации) и полиморфизмом. Да еще и обильно сдобренное обобщенным программированием.



Disclaimer



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



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



Что-то вроде заключения



ООП — это всего лишь инструмент. Не религия, не болезнь. Всего лишь инструмент. Где-то он уместен, где-то нет. Скажем, если вам нужно делать сложную и большую библиотеку, то ООП может вам пригодиться. Если делаете небольшое и несложное приложение, то может и не пригодиться. А может и наоборот. Тут все зависит как от предметной области, так и от ваших знаний и опыта. Ну и от религиозных пристрастий, конечно же.



Аналогично и с обобщенным программированием. Это всего лишь инструмент. Вас никто не заставляет его использовать, когда в этом нет нужды.



Но вот мне лично не очень удобно, когда набор доступных мне инструментов преднамеренно ограничивается. Либо путем изъятия ООП (или превращения ООП в какое-то жалкое подобие оного). Либо путем изъятия шаблонов-дженириков. Еще хуже, когда нет ни того, ни другого. Поскольку эти инструменты были созданы для упрощения работы программиста. Странно от них отказываться по доброй воле.



Ну а C++, при всех своих недостатках, хорош тем, что позволяет использовать и то, и другое. Да еще в самых разных сочетаниях. Другой вопрос, как научиться использовать и то, и другое (и еще кучу возможностей C++) по месту и в меру. Но это уже совсем другая история… :)

Источник: Хабрахабр

Категория: Компании » Google

Уважаемый посетитель, Вы зашли на сайт как незарегистрированный пользователь.
Мы рекомендуем Вам зарегистрироваться либо войти на сайт под своим именем.

Добавление комментария

Имя:*
E-Mail:
Комментарий:
Полужирный Наклонный текст Подчеркнутый текст Зачеркнутый текст | Выравнивание по левому краю По центру Выравнивание по правому краю | Вставка смайликов Выбор цвета | Скрытый текст Вставка цитаты Преобразовать выбранный текст из транслитерации в кириллицу Вставка спойлера
Введите два слова, показанных на изображении: *