В свое время я написал для журнала «Хакер» цикл статей для рубрики «Академия С++», в котором описывал интересные возможности использования C++. Цикл давно завершён, но меня до сих пор часто спрашивают, как именно работает эмуляция динамической типизации из первой статьи. Дело в том, что когда я начинал цикл, не знал точно, что нужно, а что нет, и упустил в описании ряд нужных фактов. Зря! В обучающем материале не бывает ничего лишнего. Сегодня я в деталях изложу, как именно получается красивый высокоуровневый API в терминах самого обычного C++: просто классы, методы и данные.

Для чего это нужно


Как правило, на C++ пишется что-то быстрое, но не всегда удобное в использовании. В процессе разработки любого продукта выделяется общий функционал с худо-бедно оформленным интерфейсом работы с сущностями продукта. Язык C++ всячески поощряет указатели и ссылки на базовые классы, которые множатся и усложняют код, заворачиваются во всевозможные «умные» указатели и порождают километровые строки при любом обращении к подобной конструкции!
Согласитесь, вряд ли удобно использовать такое:
	std::unordered_map<std::string, std::vector< std::shared_ptr<base_class>>>

Особенно если для каждого элемента вектора нужна операция класса-наследника, то есть метод не входит в вышеупомянутый base_class. Что, не можете найти base_class в конструкции чуть выше? А я о чём говорил!
Для удобства использования работы с базовым классом проще всего выделить сущность работы с ним и инкапсулировать в неё интерфейс как простой указатель на данные класса.

Интерфейс базового класса


Чтобы максимально упростить повествование, будет много примеров, и мы не будем отходить далеко от кода. Сам код будет прилагаться к статье, и здесь его нигде и никто уже не потеряет. Итак, базовый класс предлагаю оформить как объект данных, давайте максимально его упростим:

class object
{
public:
    object(); // по умолчанию создание без данных, аналог null
    virtual ~object(); // для корректной генерации unique_ptr
    // копирование
    object(const object& another);
    object& operator = (const object& another);
    // проверка на null
    bool is_null() const;
    // объявление типа спрятанного в реализации
    class data;
    // для работы с потомками
    const data* get_data() const;
    // это понадобится для тестирования
    const char* data_class() const;
protected:
    // инициализация в потомках
    object(data* new_data);
    void reset(data* new_data);
    // это нужно для работы с данными
    void assert_not_null(const char* file, int line) const;
private:
    // для простоты изложения
    std::unique_ptr<data> m_data;
};


То, что мы ранее использовали в качестве интерфейса на базовый класс, превращается у нас в object::data — важнейший класс, который теперь не виден нигде снаружи.
На самом деле, в object, как и в object::data, должны присутствовать базовые операции, для которых и был заведён тот самый base_class. Но нам они в описании не понадобятся, и без того будет много интересного.
В минимальном виде класс данных объекта выглядит проще некуда:

class object::data
{
public:
    // самый важный метод класса данных
    virtual data* clone() const = 0;
    // это понадобится для тестирования
    virtual const char* class_name() const = 0;
};


Единственный метод, который нам действительно понадобится в базовом классе — это клонирование данных соответствующего наследника. Причём, как можно было заметить, интерфейсный класс прекрасно обходится без метода clone(), сам object и все его наследники пользуются обычными конструкторами копирования. Вот здесь мы и подходим к самому главному — наследованию от инкапсулированного базового класса.

Двойное наследование


Для наследников нам нужно выбрать пару сущностей. Давайте будем разрабатывать компьютерную игру, где у нас будут космические корабли и астероиды. Соответственно, нам нужны две пары классов для работы: asteroid и spaceship.
Давайте добавим по уникальному методу классам наследникам: пусть астероиды различаются по целочисленному идентификатору, а космические корабли идентифицируются уникальным именем:

class asteroid : public object
{
public:
    // пусть астероидов без идентификатора не бывает
    asteroid(int identifier);
    // копируем астероид
    asteroid(const asteroid& another);
    asteroid& operator = (const asteroid& another);
    // понадобится для приведения типа "наверх"
    asteroid(const object& another);
    asteroid& operator = (const object& another);
    // уникальный метод класса-наследника
    int get_identifier() const;
    // собственный класс данных
    class data;
private:
    // ссылка на интерфейс своего (!) класса данных
    data* m_data;
};

class spaceship : public object
{
public:
    // да не будет безымянных кораблей
    spaceship(const char* name);
    // копируем данные корабля
    spaceship(const spaceship& another);
    spaceship& operator = (const spaceship& another);
    // понадобится для приведения типа "наверх"
    spaceship(const object& another);
    spaceship& operator = (const object& another);
    // уникальный метод класса "получить имя"
    const char* get_name() const;
    // свой класс данных
    class data;
private:
    // ссылка на свои (!) методы и свойства
    data* m_data;
};


Обратите внимание, что несмотря на то, что роль контейнера выполняет предок object, в наследниках есть ссылка на содержимое object, но уже нужного типа. Наследование основных классов также должно быть продублировано для классов данных (ниже я покажу, для чего это нужно):

class asteroid::data : public object::data
{
public:
    // данные астероида создаются только с идентификатором
    data(int identifier);
    // получение идентификатора доступно только для астероида
    int get_identifier() const;
    // вот эта перегрузка крайне важна!
    virtual object::data* clone() const override;
    // эта перегрузка понадобится только для теста
    virtual const char* class_name() const override;
private:
    // данные класса asteroid известны только в реализации
    int m_identifier;
};

class spaceship::data : public object::data
{
public:
    // имя обязательно, без него звездолёт с данными не создать
    data(const char* name);
    // запросить имя можно только через интерфейс spaceship::data
    const char* get_name() const;
    // очень важно перегрузить этот метод!
    virtual object::data* clone() const override;
    // понадобится для тестирования и наглядности
    virtual const char* class_name() const override;
private:
    // только в реализации нам и понадобится #include <string>
    std::string m_name;
};


Теперь чуточку подробнее пройдём по реализации, и всё сразу встанет на свои места.

Реализация методов


Создание экземпляра непосредственно типа object конструктором по умолчанию будет означать создание объекта с null-значением.

object::object()
{
}

object::~object()
{
}

object::object(object::data* new_data)
    : m_data(new_data)
{
}

object::object(const object& another)
    : m_data(another.is_null() ? nullptr : another.m_data->clone())
{
}

object& object::operator = (const object& another)
{
    m_data.reset(another.is_null() ? nullptr : another.m_data->clone());
    return *this;
}

bool object::is_null() const
{
    return !m_data;
}

const object::data* object::get_data() const
{
    return m_data.get();
}

const char* object::data_class() const
{
    return is_null() ? "null" : m_data->class_name();
}

void object::reset(object::data* new_data)
{
    m_data.reset(new_data);
}

void object::assert_not_null(const char* file, int line) const
{
    if (is_null())
    {
        std::stringstream output;
        output << "Assert 'object is not null' failed at file: '" << file << "' line: " << line;
        throw std::runtime_error(output.str());
    }
}


Теперь самое главное, как же инициализируются экземпляры классов-наследников:

asteroid::asteroid(int identifier)
	: object(m_data = new asteroid::data(identifier))
{
}

spaceship::spaceship(const char* name)
	: object(m_data = new spaceship::data(name))
{
}


Как видно из этих нескольких строк, мы убиваем сразу стадо зайцев одни залпом фазового бластера:
  1. мы получаем создание наследников с сохранением ссылки на данные в специальный класс-контейнер обычным конструктором;
  2. класс-контейнер является также и базовым классом для всех прочих, вся основная работе по хранению интерфейса делается в базовом классе;
  3. класс-наследник имеет интерфейс для работы с классом данных соответствующего класса в m_data;
  4. работаем мы с самыми обычными классами, не по ссылке, получая все плюшки автоматизации C++ работы с экземплярами классов.


Разумеется при обращении к данным соответствующий класс будет использовать свой интерфейс-наследник, при этом проверяя данные на null:

int asteroid::get_identifier() const
{
    assert_not_null(__FILE__, __LINE__);
    return m_data->get_identifier();
}

const char* spaceship::get_name() const
{
    assert_not_null(__FILE__, __LINE__);
    return m_data->get_name();
}


Простой пример, который будет работать как часы:

	asteroid aster(12345);
	spaceship ship("Alfa-Romeo");
	object obj;
	object obj_aster = asteroid(67890);
	object obj_ship = spaceship("Omega-Juliette");


Проверяем:
Test for null:
aster.is_null(): false
ship.is_null(): false
obj.is_null(): true
obj_aster.is_null(): false
obj_ship.is_null(): false

Test for data class:
aster.data_class(): asteroid
ship.data_class(): spaceship
obj.data_class(): null
obj_aster.data_class(): asteroid
obj_ship.data_class(): spaceship

Test identification:
aster.get_identifier(): 12345
ship.get_name(): Alfa-Romeo


Не правда ли, напоминает высокоуровневые языки: C#, Java, Python и т.п.? Единственную сложность составит получение обратно интерфейса наследников, запакованных в object. Сейчас мы научимся извлекать в экземпляры asteroid и spaceship то, что ранее было запаковано в object.

Путь наверх


Всё, что нам нужно, это перегрузить конструктор классов-наследников, правда сама инициализация при этом получится не очень:

asteroid::asteroid(const asteroid& another)
    : object(m_data = another.is_null() ? nullptr : static_cast<asteroid::data*>(another.get_data()->clone()))
{
}

asteroid& asteroid::operator = (const asteroid& another)
{
    reset(m_data = another.is_null() ? nullptr : static_cast<asteroid::data*>(another.get_data()->clone()));
    return *this;
}

asteroid::asteroid(const object& another)
    : object(m_data = (dynamic_cast<const asteroid::data*>(another.get_data()) ?
                       dynamic_cast<asteroid::data*>(another.get_data()->clone()) : nullptr))
{
}

asteroid& asteroid::operator = (const object& another)
{
    reset(m_data = (dynamic_cast<const asteroid::data*>(another.get_data()) ?
                    dynamic_cast<asteroid::data*>(another.get_data()->clone()) : nullptr));
    return *this;
}


spaceship::spaceship(const spaceship& another)
    : object(m_data = another.is_null() ? nullptr : static_cast<spaceship::data*>(another.get_data()->clone()))
{
}

spaceship& spaceship::operator = (const spaceship& another)
{
    reset(m_data = another.is_null() ? nullptr : static_cast<spaceship::data*>(another.get_data()->clone()));
    return *this;
}

spaceship::spaceship(const object& another)
    : object(m_data = (dynamic_cast<const spaceship::data*>(another.get_data()) ?
                       dynamic_cast<spaceship::data*>(another.get_data()->clone()) : nullptr))
{
}

spaceship& spaceship::operator = (const object& another)
{
    reset(m_data = (dynamic_cast<const spaceship::data*>(another.get_data()) ?
                    dynamic_cast<spaceship::data*>(another.get_data()->clone()) : nullptr));
    return *this;
}


Как видно, здесь придётся использовать dynamic_cast, просто потому что приходится идти вверх по иерархии классов данных. Выглядит массивно, но результат того стоит:

	object obj_aster = asteroid(67890);
	object obj_ship = spaceship("Omega-Juliette");
	asteroid aster_obj = obj_aster;
	spaceship ship_obj = obj_ship;


Проверяем:
Test for null:
aster_obj.is_null(): false
ship_obj.is_null(): false

Test for data class:
aster_obj.data_class(): asteroid
ship_obj.data_class(): spaceship

Test identification:
aster_obj.get_identifier(): 67890
ship_obj.get_name(): Omega-Juliette


Туда и обратно. Как у Толкиена, только значительно короче.
Не забываем протестировать также и операторы присвоения:

    aster = asteroid(335577);
    ship = spaceship("Ramambahara");
    obj = object();
    obj_aster = asteroid(446688);
    obj_ship = spaceship("Mamburu");
    aster_obj = obj_aster;
    ship_obj = obj_ship;


И снова проверяем:
Test for null:
aster.is_null(): false
ship.is_null(): false
obj.is_null(): true
obj_aster.is_null(): false
obj_ship.is_null(): false
aster_obj.is_null(): false
ship_obj.is_null(): false

Test for data class:
aster.data_class(): asteroid
ship.data_class(): spaceship
obj.data_class(): null
obj_aster.data_class(): asteroid
obj_ship.data_class(): spaceship
aster_obj.data_class(): asteroid
ship_obj.data_class(): spaceship

Test identification:
aster.get_identifier(): 335577
ship.get_name(): Ramambahara
aster_obj.get_identifier(): 446688
ship_obj.get_name(): Mamburu


Всё работает как надо! Ниже идёт ссылка на GitHub с исходниками.

PROFIT!


Что мы имеем? Это не Pimpl, для Pimpl здесь слишком много полиморфизма, да и название «указатель на реализацию» не самое удачное. В C++ реализация и так находится отдельно от объявления класса, в .cpp файлах, Pimpl позволяет убрать данные в реализацию. Здесь данные не просто прячутся в реализацию, они составляют дерево иерархии, при этом зеркально отражая иерархию интерфейсных классов. Вдобавок мы получаем инкапсуляцию null-значений и можем встраивать логику допустимости null-значений в классы-наследники. Все классы легко жонглируют данными — как своими, так и всей цепочкой предков и наследников, при этом сам синтаксис будет прост и лаконичен.
Хотите сделать просто в API своей библиотеки? Теперь вам ничего не мешает. Что до реплик о том, что C++ очень сложен и на нём нельзя сделать высокоуровневую логику — пожалуйста, можно комбинировать массивы таких объектов, не хуже C# или Java, при этом преобразования будут даже проще. Вы можете сделать ваши классы простыми в использовании, при этом не понадобится хранить указатели на базовый класс, возиться с фабриками, в общем, всячески эмулировать обычные конструкторы и операторы присвоения больше не придётся.

Полезные ссылки


Со статьёй идут исходники, выложенные на GitHub.
Исходники дополнены парой методов, которые упрощают тестирование и позволяют быстрее понять, как работает передача данных между объектами.
Также оставлю ссылку на цикл статей «Академия C++» для журнала «Хакер».

Комментарии (22)


  1. Hokum
    17.09.2015 10:05

    С удовольствием прочитал статью, идея интересная. Но для меня странным выглядит конструкция:

        ship = spaceship("Ramambahara");
        object obj = asteroid(335577);
        ship = obj; //вот после это совсем как-то не ожидаешь получить невалидный объект
        if (ship.is_null()) //что-то символичное в этом есть...
        {
            std::cout << "нет больше корабля, его уничтожил астероид...\n";
            return;
        }
    


    Когда работаю на прямую с указателями на интерфейсы, то необходимость проверки результата приведение вверх по иерархии (или предварительной проверки возможности приведения) очевидна сама по себе. Если, конечно задумываться, что возвращает dynamic_cast. А вот в случае с объектами это может ускользнуть.
    А в начале делать присвыоение к временному объекту, потом делать проверку на его валидность и только после этого делать финальное копирование — довольно накладно. Хотя использование move семантики уменьшит накладные расходы.


    1. Qualab
      17.09.2015 10:33

      Вы правы, всё что нужно — это добавить конструктор перемещения object(object&&) и оператор перемещения operator = (object&&). В целом можно вообще разделить объекты по ссылке и по значению как из второй статьи цикла Академии с оптимизацией placement new для небольших классов и скаляров. Также можно внутри класса в принципе запретить null-значения, то есть ругаться на object без данных и на dynamic_cast, который вернул null. В целом идею можно развивать до полноценной библиотеки или встраивать в существующую, профит весьма привлекателен.


      1. Hokum
        17.09.2015 10:46

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

        Я не против таких идей, они раскрывают богатство языка, но не стоит их класть в основу разработки на C++. Я читал выпуски Академии, но считаю — не нужно совершать «побег из темницы типов». Бывают случаи когда без этого код получается уродливым, тогда да, но это пусть это будет маленький кусочек в большом проекте. А если таким становится весь проект, то нужно менять язык или архитектуру приложения.


    1. Hokum
      17.09.2015 10:33

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

      Если очень хочется или часто приходится искать лазейки в строгой типизации C++, то стоит задуматься о смене языка, тем более, что такие вещи отъедают ресурсы и производительность падает.

      Иногда такое можно использовать, но базировать на таких методах разработку на C++ не стоит.


      1. Qualab
        17.09.2015 10:53
        +1

        Всё на самом деле гораздо интереснее. Сейчас C++ очень часто встраивается в высокоуровневые библиотеки бизнес-логики и писать биндинги под такие обобщённые объекты на порядок проще. Соответственно преобразования в те же Python и C# получаются крайне простыми и код будет почти одинаковым, что важно. Это верно и в обратную сторону, в игровой индустрии например C++ состовляет основу движка и поверх идёт скриптовая обвязка, оптимизировать данный подход несложно, а разработчики будут легко работать как внутри движка, так и в расширениях. Сейчас скорость разработки на вес золота.
        Кроме всего прочего очень часто библиотеки общего пользования очень часто изобилуют Pimpl'ами. Данный подход позволяет оптимизировать расходы на разработку «классов реализации» выстроив иерархию классов данных, что опять же положительно скажется на скорости разработки.
        Не стоит также забывать на скорость вхождения в работу новых людей. Код получается простым и понятным, в нём проще разобраться, новички быстро втягиваются и пишут полноценный код. Опять же увеличивая скорость разработки.


        1. Hokum
          17.09.2015 11:50

          Я не спорю про скорость разработки. Я к тому, что за это приходится платить скоростью работы приложения (смотря что за приложение, конечно). Известные библиотеки хорошо, но если эта билиотека позволяет писать на одном языке так как буд-то пишешь на другом, то может стоит просто взять другой язык? И тогда искать сотрудников становится еще проще.

          Языки C#, Python, подкупают своей простотой, но за это мы платим скоростью работы приложения, точнее сказать может быть медленее, и все как-то к этому привыкли, хотя понится были упреки в их адрес, что они медленные. Но эта «простота» увеличивает порог вхождения — для правильной разработки, нужно больше знать об устройстве языка, какие механизмы он использует, как работает, что кроется за простым "=".

          Это же относится и к вашему решение, я не пытаюсь преуменьшить его интересность и важность.

          Pimpl'ы позволяют сохранить работоспособность при использовании разных версий библиотек, когда интерфейс сохраняется, а внутреннее устройство поменялось. И ваше решение их не заменяет.

          В общем каждому инструменту свое время и место. И хорошо когда есть обширный выбор.

          P.S.
          Возможно, я просто, не могу оценить перпективы использование подобных вещей в связке с другими языками (из-за отсутсвия такого опыта) и рассматриваю только применительно к чистому C++.


          1. Qualab
            17.09.2015 12:34

            Скорость разработки с использованием таких классов прямо пропорциональна скорости разработки приложения. Скорость разработки интерфейсных классов при этом не отличается от разработки из с применением паттерна Pimpl.
            В некоторых случаях у нас нет выбора и часть функционала надо писать на C++, при этом предоставляя удобный интерфейс для работы другим разработчикам.
            Pimpl'ы повзоляют легко менять реализацию. В приведённом коде так же легко можно менять поля и реализацию классов object::data с наследниками. Здесь преимуществ не меньше, а больше. Этот подход гибче и дополнительные классы более обоснованы.
            Связка с другими языками это хорошо, но данный подход будет отлично работать и сам по себе в C++ для C++, предоставляя удобный способ работы с иерархией классов.


  1. Hokum
    17.09.2015 11:50

    не туда написал…


  1. eastig
    17.09.2015 15:23
    +1

    В качестве альтернативы стандартному RTTI можно использовать LLVM-style RTTI (HowToSetUpLLVMStyleRTTI.html). Так как внутри компилятора приведение типов по иерархии используется интенсивно, он разрабатывался так, чтобы с одной стороны быть достаточно гибким, а с другой стороны иметь меньше накладных расходов, чем стандартный RTTI.


    1. mapron
      17.09.2015 15:42
      +1

      В Qt тоже используется схожий подход. например, в QGraphicsItem и иже с ним.


      1. Qualab
        17.09.2015 16:06

        Чтобы было ближе к Qt имеет смысл сделать copy-on-write для данных. То есть заменить на небольшую обвязку наз std::shared_ptr с перегрузкой operator -> с const. Вообще действительно похоже.


        1. mapron
          18.09.2015 17:07

          Да RTTI-то тут при чем… Я только про него @eatsig сказал.
          А так да, можно аналогии и с Вашей статьей провести.


    1. Qualab
      17.09.2015 16:04

      Спасибо, посмотрю.


  1. skor
    17.09.2015 15:46

    Опять дочитал до слова virtual и запнулся:

    virtual ~object(); // для корректной генерации unique_ptr
    

    Поясните, пожалуйста, что вы имеете в виду?


    1. Qualab
      17.09.2015 16:04

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


    1. Door
      19.09.2015 15:09

      Погуглите pimpl unique_ptr: 1, 2, 3


      1. skor
        20.09.2015 16:31

        Спасибо, я в курсе. Для пимпла на базовый класс опасно и затратно использовать unique_ptr с виртуальным деструктором, тогда уж shared_ptr + защищенный невиртуальный декструктор. Во всех примерах, что вы привели, unique_ptr в пимпле не используется для хранения указателя на базовый класс. Вследствие несистемности подхода получаем, например, утечку в object::data.


        1. Qualab
          21.09.2015 19:19

          В object::data конечно же нужен виртуальный деструктор. Вечером всё поправлю. Извините за неточность. Вообще shared_ptr тоже не вариант, больше подойдет комбинация подходов из первых двух статей: by-value или copy-on-write — оптимально и без дополнительных затрат.


  1. dyadyaSerezha
    21.09.2015 18:00

    И вся эта бодяга для того, чтобы написать это??

    object obj = asteroid(335577);
    asteroid = obj;

    И для этого создается каждый раз лишний объект на куче? И пишется двойная иерархия классов с дублированными методами? И это называется «быстро пишется и быстро работает»? И главное — зачем??

    Да и само название уже звучит смешно — инкапсуляция интерфейсов. Примерно как «деланье публичными приватных данных».


    1. Qualab
      21.09.2015 19:18

      Объект на куче прекрасно оптимизируется, во-первых by-value из материала второй статьи через placement new для небольших объектов, во-вторых через copy-on-write для больших объектов. Двойная логика работы с классами в больших проектах всё равно пишется, если используется Pimpl, здесь подход позволяет осмыслить классы данных, спрятанные в реализации, через аналогичное дерево иерархии. Зачем: мы пишем код без указателей и ссылок, работая именно с экземплярами классов, а не с какими-то малопонятными ссылками на то, что создала фабрика и выдала нам в виде неудобной синтаксической конструкции в лучшем случае или в виде raw указателя в худшем.
      Если вы инкапсулируете привычные интерфейсы типа ISomething* в удобные классы работы с этими интерфейсами Something, то это хорошо. Если вам платят за то, что вы просто пишете код, который хоть как-то работает, не расширяем и трудно читаем — это тоже неплохо (но не для новичка разбирающего ваш код, конечно же).


      1. dyadyaSerezha
        21.09.2015 20:48

        При чем тут copy-on-write, если вы создаете 100К разных объектов, например?

        Далее…

        Язык C++ всячески поощряет указатели и ссылки на базовые классы, которые множатся и усложняют код, заворачиваются во всевозможные «умные» указатели

        Не поощряют, но дают такую возможность, если есть необходимость.

        и порождают километровые строки при любом обращении к подобной конструкции!
        Согласитесь, вряд ли удобно использовать такое:
        std::unordered_map<std::string, std::vector< std::shared_ptr<base_class>>>

        А слабо написать:
        using Asteroid = std::unordered_map<std::string, std::vector< std::shared_ptr<base_class>>>;
        или
        typedef std::unordered_map<std::string, std::vector< std::shared_ptr<base_class>>> Asteroid;
        и дальше использовать только тип Asteroid? Хотя в любом случае никто не определяет внешний API через такие типы.

        Особенно если для каждого элемента вектора нужна операция класса-наследника, то есть метод не входит в вышеупомянутый base_class

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

        Вывод: есть случаи, когда ваш вариант оправдан, но далеко не всегда.


        1. Qualab
          21.09.2015 21:04

          Есть такой же typedef для std::string. Мы с Вами оба видим замечательные стеки с кучей ненужной информации во время ошибок компиляции, линковки или логирования через typeid: std::basic_string<… std::allocator <… > > и т.д. Особенно если мы специализируем шаблон от std::string, там вообще песня получается. Если перегрузка от std::string, или конструкции, содержащей подобный typedef, тоже получаем километровую неинформативную простыню. Вам это точно надо? Вы хотите множить подобный подход? Одно дело через using отсекать лишние упоминания namespace'ов — пространства имён как раз благо, а совсем другое — создавать видимость, что всё хорошо, когда всё крайне запущено, запутано и переусложнено.
          API обязан быть простым, понятным и в нём не должно быть ничего лишнего. Sine qua non.
          Разработчик, использующий Ваш API не должен через раз делать dynamic_cast, static_cast или, не дай бог, const_cast, только потому, что разработчик API не предусмотрел очевидные use cases. Всё должно быть логично и максимально минималистично в использовании.