О бедном C++ API замолвите словцо!

  • Tutorial
Желание написать об C++ API у меня возникло давно, и вот наконец выдался спокойный вечер. По роду деятельности я и мои ребята пишем код на C++ для программистов на C++ и Python, общее ядро функционала, который используется во всех продуктах нашей компании. Разумеется это подразумевает, что код должен иметь интуитивно понятный API, с общей логикой как для низкоуровневого C++, так и для высокоуровневого Python, вне зависимости от разночтения в языках некоторых базовых конструкций. Об объединении C++ и Python я много писал ранее в статьях про Boost.Python, сейчас я очень благодарен архитектуре и логике языка Python, я многое понял и перенял в С++ именно благодаря опыту построения общего API для этих двух таких разных языков, но сейчас речь пойдёт только и исключительно о C++, про API и про то, что такой зверский гибкий язык позволяет сделать с интерфейсом вашей замечательной библиотеки, если не учитывать ряд важных особенностей языка C++.

Present perfect coninuous


Итак, на дворе 2014 год. Язык C++ (в отличие от его предка-подмножества языка Си) обладает всем потенциалом языка высокого уровня, который уже на протяжении многих лет остаётся не более чем потенциалом. Если разработчику не хватает чего-то высокоуровневого, придётся либо изобретать что-то своё, либо пытаться найти готовое решение, благо источников готового кода более чем предостаточно. Это и старый-добрый STL, который в C++11 немного заштопали и обновили, это и монструозный Boost, с разношёрстными библиотеками разной степени готовности, есть и Qt с его интерфейсными и не только плюшками, а также море свободно распространяемого софта с разной степени заразности лицензиями. Я не буду сравнивать богатство библиотек C++ с библиотеками для языков Java и Python, всё-таки языки решают разные задачи, но каждому очевидно, что возможности написать на C++ что-то высокоуровневое без жёсткого порно траты лишнего времени не выйдет. Итак, рано или поздно, определив свою нишу, любой разработчик С++ пишет для себя или для общества, возможно для коллег, некий общий функционал, объединяя разношёрстное разноцветия множества API в единую стройную структуру, в поиска своего дзена проектирования API. Бывают конечно люди, предпочитающие только пользоваться библиотеками, написав код, за который им платят, не интересуясь красотой построения программного интерфейса, променяв его на мирскую суету, но речь сейчас не о них… Эта статья для тянущихся к свету истинного проектирования прекрасного API языка C++, ну и немного для тех, кого к этому свету насильственно притягивают за уши.

Классы


Как ни странно, но именно классы являются тем самым слабым звеном вашего API, если вы стремитесь сделать интерфейс вашей библиотеки максимально прозрачным и предсказуемым. Причём даже без учёта шаблонов этих самых классов. Если немного окунуться в историю, то класс в C++ это лишь надстройка на структурой struct в языке Си. Все свои поля содержит по значению, то есть является ничем иным как совокупностью своих полей с некоторой надстройкой ООП. Все кто как-либо касался Managed C++ расширения языка C++ для платформы .NET, помнят, что классы могут ссылаться на свои данные по ссылке, а могут по значению. Мы не будем обсуждать ужасы тонкости взаимодействия языка C++ с фреймворком .NET, просто заметим, что в обычном C++ данные класса всегда хранятся по значению, даже если это значение — ссылка или указатель. Так что мы не очень далеко ушли от языка Си, в котором по значению передавались даже аргументы в функцию и без вариантов, хочешь ссылку — передай указатель. Всё это имеет неоспоримые преимущества при размещении на стеке временных переменных и это же размещение данных по значению является вероятно самой большой проблемой при архитектуре более-менее высокоуровневой библиотеки, подразумевающей интенсивное развитие, выпуск новых версий и совместимый программный интерфейс с каждой последующей версией. Не суть важно как вы называете классы, пространства имён (namespaces), методы и поля, главное то, как вы прячете от вашего любимого пользователя вашей библиотеки детали реализации, всё то что ему знать не нужно (кому нужно сам залезет в .cpp или .cxx файлы и поглядит детали реализации) ведь в заголовочном файле не должно быть ничего кроме API, по возможности следует даже убрать все лишние #include, заменив на предварительное объявление (forward declaration) все используемые по ссылке типы…
… включая тип данных самого класса!
Да-да-да! Если есть возможность держать данные класса по ссылке, то данные можно объявить отдельным классом без реализации в заголовочном файле и вынести его целиком в .cpp-файл с реализацией методов. То есть API вашего класса сводится к следующей схеме:

// до этого должно быть объявлен аналог SOME_API переключая import / export при сборке (если необходимо!)
// мы же разрабатываем кроссплатформенную библиотеку с независимым от системы сборки API (если нет, убираем аналог SOME_API)
class SOME_API something
{
public:
    // здесь куча методов, конструкторов и операторов

private:
    class data; // неважно что там в классе, а если вы собираете SDK, не показывая исходников, этого никто и не узнает

    std::shared_ptr<data> m_data; // не лучший вариант, но пока не рассмотрели copy-on-write модель это и понятно и безобидно
};


Аналог этого кода будет в вашем заголовочном файле вне зависимости от расширения: .h, .hpp, .hxx либо вообще его отсутствия. Здесь не так важно именование, как важен принцип. Вне зависимости от наполнения класса something::data мы можем не менять файл с API класса something, точнее не теряя совместимость с его предыдущими версиями. Но и это ещё не всё! © Классический подход с обычным хранением по значению таит в себе ещё целый выводок подводных скал, на которых нас несёт течение языка C++ с поведением объектов вашего класса по умолчанию.
Чтобы полнее ощутить всю прелесть хранения по значению рассмотрим небольшой пример:

// здесь и далее в примерах аналог SOME_API опускаем
class person
{
public:
    // не суть важно с какой стороны от типа вы пишете const (хотя для констант указателей это важно)
    void set_name(std::string const& name);

    // по-хорошему возвращать результат нужно по значению, но в STL от MS данные std::string будет копироваться
    // почему это так, и что такое copy-on-write, об этом ниже
    std::string const& get_name() const;

    // дадим полный доступ для vector of child, просто потому что это пример, удобный для объяснения
    // но такой способ возврата приковывает нас к единственному способу хранения поля m_children, поэтому так делать плохо
    std::vector<person>& get_children();

    // для того чтобы и здесь вернуть объект списка детей по значению, нужно завести отдельный класс с удобным API
    // либо сделать ряд удобных методов вида add_child, get_child и т.п.
    std::vector<person> const& get_children() const;

private:
    std::string m_name; // какое-то имя человека, представимого классом person
    std::vector<person> m_children; // список детей человека, каждый из которых тоже почему-то человек
};


Итак, что мы здесь видим? Конструктор по умолчанию, который порой суров к POD-типам, здесь генерируется вполне сносный, поскольку поля — стандартные контейнеры STL, у них с инициализацией по умолчанию всё в порядке. В целом вы должны иметь в виду, что конструкторы имеют свойство генерироваться: конструктор по умолчанию, конструктор копирования и в C++11 ещё и конструктор перемещения, также сгенерируются оператор копирования и для C++11 оператор перемещения.
И если для инициализации и перемещения здесь всё чисто благодаря STL, то вот с копированием благодаря тем же контейнерам STL мы получим ад рекурсивного копирования.
Простая операция:

person neighbour = granny;

может привести к адской головной боли несчастного разработчика, использующего ваш класс person. Представьте что в переменной granny находится построенный объект некой бабушки с богатым потомством, у неё есть куча детей и по каждому из детей ещё и на порядок больше внуков. По всем правилам все эти замечательные потомки бабушки начнут клонироваться как амёбы в объект neighbour. Так будет конечно же безопасно для бабушки, в плане дзена С++ и контейнеров STL, но совсем небезопасно для её психики, да и для оптимизации выполнения базовых операций при работе с вашим классом тоже очень и очень плохо.
Нерадивый разработчик скажет: «ой, да ладно, разберутся, не маленькие», и несчастные пользователи библиотеки будут думать, как же забороть тот самый вектор внутри каждого объекта, который рекурсивно усложняет проблему неявного копирования. И скорее всего найдут другую библиотеку, которая и спроектирована лучше, и работает более прозрачно и предсказуемо.
Неожидаемое поведение при очевидных операциях — это головная боль разработчика функционала и проектировщика API, а уж никак не несчастного пользователя их библиотеки. В крайнем случае опытные разработчики обойдут эту проблему, помещая такие опасные классы в обёртки с умными указателями, но в целом так делать нельзя. Давайте вылечим бабушку и её потомков от синхронного амёбного деления. Не оставлять же её тут в таком виде!

// это в заголовочном файле
class person
{
public:
    // нам пока понадобится конструктор по-умолчанию, потом мы от него избавимся
    person();

    // задать имя, здесь без вариантов
    void set_name(std::string const& name);

    // получить константную ссылку на имя
    // (мы помним что возвращение ссылки ограничивает возможности реализации!)
    std::string const& get_name() const;

    // получить ссылку на список потомков
    // (мы помним что возвращение ссылки ограничивает возможности реализации!)
    std::vector<person>& get_children();

    // получить константную ссылку на список потомков
    // (мы помним что возвращение ссылки ограничивает возможности реализации!)
    std::vector<person> const& get_children() const;

private:
    class data; // вообще для double dispatch лучше перенести class data в protected, но это отдельная тема

    std::shared_ptr<data> m_data; // общие данные могут быть проблемой, об этом ниже
};

// это в файле спрятанном в реализации, возможно в отдельном заголовочном
class person::data
{
public:
    void set_name(std::string const& name);
    std::string const& get_name() const;

    std::vector<person>& get_children();
    std::vector<person> const& get_children() const;
    
private:
    std::string m_name;
    std::vector<person> m_children;
};

// это уже детали реализации
person::person()
    : m_data(new data) // можно сделать инициализацию ленивой, т. е. по первому требованию данных
{
}

// далее множество методов person пробрасывающих вызов в person::data
// возможно с какими-то дополнительными действиями, например логированием или сохранением стека вызовов

// для примера:
void person::set_name(std::string const& name)
{
    // если нужно, вставляем проверку на ленивую инициализацию
    m_data->set_name(name); // здесь можно перегрузить operator-> у некоего внутреннего класса, об этом ниже
}


Итак бабушка перестала копироваться. Совсем. Что тоже не совсем правильно в концепции C++, с этим мы отдельно разберёмся. В принципе можно взять за основу Java-style и все подобные суровые значения передавать по ссылке и только по ссылке, но это значит обмануть пользователя вашей библиотеки в ситуации, когда его можно и не обманывать. Что нам мешает копировать данные этой бабушки только при изменении данных?

Copy-on-write


Метод копирования при изменении довольно прост и его довольно быстро можно реализовать самому, причём потокобезопасно (при условии потокобезопасности реализации std::shared_ptr стандартной библиотеки C++). Суть метода C-o-w в следующем, пока объект передаётся в методы как const this, данные разделяются между всеми копиями объекта порождённого от одних и тех же данных. Однако как только this становится неконстантным, любой из методов (бывают правда исключения) не помеченный как const в первую очередь проверяет std::shared_ptr::is_unique() и если на одни и те же данные ссылается больше одного объекта, мы отцепляем себе свою уникальную копию от общих данных и её правим. В принципе можно обойтись даже и без мьютекса, в худшем случае лишний раз скопируем объект, что не смертельно для тестового примера при объяснении данной темы. Реализуется же данный механизм проще всего через перегрузку operator-> для const и не-const случая this объекта промежуточного шаблонного класса, шаблонного, поскольку механизм общий. Выглядит этот промежуточный шаблон примерно так:

// крайне ленивый класс! лениво создаёт и копирует данные, то что нужно!
template <class data>
class copy_on_write
{
public:
    // обычный вызов метода, если данные ещё не были созданы, то они инициализируются
    data const* operator -> () const;

    // вызов метода, подразумевающий изменение данных
    // если ссылка на данные не уникальная, мы "отцепим" себе свою копию данных
    data* operator -> ();

private:
    mutable std::shared_ptr<data> m_data; // mutable он исключительно для ленивой инициализации в const вызовах

    void ensure_initialized() const; // реализуем ленивую инициализацию
    void ensure_unique(); // реализуем ленивое копирование
};

// реализация довольно проста
template <class data>
data const* copy_on_write<data>::operator -> () const
{
    ensure_initialized(); // убеждаемся что мы не ссылаемся на nullptr
    return m_data.get(); // возвращаем константную ссылку на данные для вызова const-метода
}

template <class data>
data* copy_on_write<data>::operator -> ()
{
    ensure_unique(); // убеждаемся, что мы единственные владельцы ссылки на общие данные
    return m_data.get(); // возвращаем ссылку на данные для вызова метода потенциально изменяющего данные
}

template <class data>
void copy_on_write<data>::ensure_initialized() const
{
    if (!m_data)
    {
        m_data.reset(new data); // потребуется конструктор по умолчанию, но в принципе с помощью type_traits это можно обойти
    }
}

template <class data>
void copy_on_write<data>::ensure_unique()
{
    ensure_initialized(); // в любом случае данные должны быть инициализированы

    if (!m_data.unique()) // метод unique() шаблона класса std::shared_ptr проверяет уникальность ссылки
    {
        m_data.reset(new data(*m_data)); // конструктор копирования класса данных должен быть доступен
    }
}

Всё что осталось, дать нашему классу person возможность жить в своё удовольствие, создавать безнаказанно вектора потомков person любой длины (они будут неинициализированы до первого обращения к данным), копировать, передавать по значение в функции (никаких const& не нужно, пусть это и чуть подороже), возвращать в качестве результата тоже по значению (опять же никаких const& и неявных ошибок из-за этого!) Но самое главное, что когда мы начнём изменять данные какого-либо объекта, скопируется только тот объект, который изменяют, от рекурсивного копирования останется только видимость!
Итак, person, живи полной жизнью и ни в чём себе не отказывай:

class person
{
public:
    // все методы остаются, конструктор по умолчанию смело убираем!

private:
    class data; // всё так же ссылаемся на данные, которые реализуем вне заголовочного файла с API

    // вот этот магический шаблон дарит нам возможность свободно дышать ссылаясь на данные класса
    copy_on_write<data> m_data; 
};

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

Константность при вызове метода


Разумеется метод выдающий ссылку на вектор — зло, крайне неудачное решение для Copy-on-write модели, особенно перегрузкой метода для const и не-const this. Просто потому что неявно может быть копирование там, где неявно используется неконстантная перегрузка. Ведь пользователь вашего API не обязан заботиться о константности своей переменной, например заводя переменную на стеке, разработчик получит неконстантную переменную. Поэтому нужно внимательно следить за тем, чтобы от константности не зависила судьба копирования объектов вашего класса. По крайней мере пусть это будет очевидно.

class person
{
public:
    void set_name(std::string const& name); // единственный метод, с которым изначально всё было в порядке

    std::string get_name() const; // убирая ссылку на std::string мы избавляемся от последней привязки API к реализации

    int get_child_count() const; // без количества потомков работать со списком потомков толком не получится

    person get_child(int index) const; // получаем потомка по индексу, учитывая что уникальность потомков именно в индексе

    void add_child(person); // просто добавляем потомка, не заставляя указывать индекс

    void set_child(int index, person const& child); // передавать объекты класса person лучше всё же по ссылке

private:
    class data; // всё так же ссылаемся на данные, которые реализуем вне заголовочного файла с API

    // вот этот магический шаблон дарит нам возможность свободно дышать ссылаясь на данные класса
    copy_on_write<data> m_data; 
};

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

Время компиляции при использовании вашего API


Бонусом получаем более шуструю компиляцию, просто потому что объявление полей person::data вынесено в отдельный файл скрытый в реализации и не нужно компилировать
#include , как в общем и сам дополнительный класс при использовании класса person извне. В принципе и сам std::string можно изменить на forward declaration, тогда в заголовочном файле можно избежать ещё и #include , ещё больше ускорив компиляцию при использовании вашего класса. Сделать это можно объявлением такого вида:

// можно вынести все эти объявления в файл вида <stdfwd> namespace std { template <typename char_type> class allocator; template <typename char_type> struct char_traits; template <typename char_type, typename traits_type, typename allocator_type> class basic_string; // forward declaration для типа std::string typedef basic_string<char, char_traits<char>, allocator<char>> string; // forward declaration для типа std::wstring typedef basic_string<wchar_t, char_traits<wchar_t>, allocator<wchar_t>> wstring; // здесь же можно добавить, чтоб не подключать лишний #include <cstddef> для std::nullptr_t typedef decltype(nullptr) nullptr_t; }

В общем старайтесь заботиться о пользователях вашей библиотеки. Время компиляции также весьма важный параметр, поскольку язык C++ довольно тяжело компилируется и при неудачном проектировании API каждая пересборка при использовании вашей библиотеки может занимать на несколько порядков больше времени, чем могла и должна бы, если б учитывалась сложность компиляции и минимализация подключения избыточных заголовочных файлов. Именно поэтому не стоит злоупотреблять метапрограммированием и излишним использованием магией шаблонов, в ней вы можете поупражняться и в файлах реализации, но это уже отдельная тема, которую я вынесу в отдельную статью. А пока вернёмся к более насущной проблеме проектирования API.

Ещё пара слов о выпечке хлеба


Все кто хоть раз читал о том "Как два программиста хлеб пекли" запомнил эту статью навсегда. Несмотря на довольно забавное повествование, статья учит главному: правильно структурировать свой код, избегая лишних сущностей. Но не все понимают, что к этим лишним сущностям приводит, а факторов всего три:
1) недостаток опыта построения API (попробуйте сами его использовать, напишите хотя бы пару тестов... ну как, удобно пользоваться?)
2) перенасыщение новым материалом красивых структурированных паттернов, которые на страницах книги выглядят так заманчиво
3) преобладание энтузиазма попробовать что-то новое над устоявшимися принципами построения API, как следствие двух предыдущих пунктов.

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

1. Странные фабрики типов, которые создают что-то заранее типа неизвестное, либо просто без основания создаётся что-то разнотипное, унаследования от одного класса-предка, обычно попытки воссоздать interface-класс из высокоуровневых языков (бывает что и без виртуального деструктора). В большинстве случаев заменяется банальным конструктором объекта-контейнера того самого интерфейса. Вместо этого над пользователем издеваются, предлагая создавать код вида:

std::unique_ptr<IAmUselessInterface> something = UserUsefulFactory::CreateSomethingLessUseless<DerivedUsefulClass>(arguments);

И это заставляют делать пользователя библиотеки, вместо того, чтобы просто немного потрудиться, реализовав double dispatch, если действительно нужно наследование, но как правило хватает банального контейнера для ссылки на одного-двух наследников, указатель на предка которых можно просто поместить в private. В результате пользователь будет просто создавать обычные объекты C++ простейшим конструктором, не задумываясь о работе с каким-то указателем на интерфейс, работая с API обычного класса-контейнера.

2. Излишняя синглтонистость. Это уже эпидемия. Как правило фабрику из пункта 1 тоже делают синглтоном. Просто потому что вчера прочитал, а сегодня хочется всё попробовать и пусть завтра это уходит на продакшн!.. Я не спорю, синглтон порой вещь незаменимая, когда требуется например отложенное либо упорядоченное создания глобальных переменных, но делать через синглтон всё, что хоть как-то напоминает класс с обычными статическими методами - это слишком! Вот например во что превращается наша старая добрая фабрика из пункта 1 применимо к выпечке хлеба:

std::unique_ptr<IХлеб> something = ФабрикаХлеба::Instance().СоздайПирожок<ПирожокСПовидлом>(
                                                           ФабрикаТеста::Instance().ДайТеста<ТестоДляПирожков>(42));
// и это всё вместо примерно такого:   Хлеб пирожок(Тесто<ДляПирожка>(42));

3. Засилье наследования. Просто запомните важное правило: там где можно наследование эффективно заменить на инкапсуляцию, нужно использовать инкапсуляцию, а не наследование. Если сомневаетесь, что использовать: наследование или инкапсуляцию - используйте инкапсуляцию. Просто потому что наследование подразумевает ряд проблем, которые вы перекладываете на пользователя вашего API, причём одна из них - это хранение где-то созданного экземпляра класса-наследника, причём видимо указателем на базовый класс, который вероятно абстрактный. Я не спорю, если C++ даёт вам возможность вытворять с классом что угодно вы вправе это делать, но вряд ли пользователь скажет спасибо от засилья наследников никому непонятного класса, который вы ввели просто потому что захотелось хоть какую-то иерархию классов. Действительно, если у сущностей Крокодил и Камень есть общая сущность МаяПридумалНовыйКласс, почему бы её не завести и не вытащить в API как общий базовый класс объединяющих данные две сущности.

Посмотрите например на реализацию интерфейса библиотеки Boost.Python. Класс object из namespace boost::python не столько предок остальных классов, представляющих объекты языка Python внутри C++, сколько контейнер для PyObject*, которым никто пользоваться не заставляет. Нужный объект PyObject* просто создаётся простым конструктором класса object по типу аргумента конструктора (конструктор по умолчанию кстати создаёт None - аналог NULL-значения в Python). Да, здесь есть наследование, но никто не заставляет работать с тем же boost::python::dict как со ссылкой на boost::python::object с кучей перегруженных методов. Нет, здесь выбран подход инкапсуляции и наследование лишь помогает, например при передаче аргументов, позволяя обобщить тип объекта, например в dict до того же object.

В целом double dispatch - отдельная большая тема, где и интерфейс класса, и инкапсулированный класс-реализация наследуются параллельно, что позволяет творить в С++ настоящие чудеса типизации зачастую безо всяких шаблонов. Чтобы увязать в вашем сознании что это такое, давайте немного вернёмся к предыдущему примеру:

class person
{
public:
    // здесь всё остаётся по-прежнему

protected:
    class data;

    data& get_data_reference();
    data const& get_data_const_reference() const;

private:
    copy_on_write<data> m_data;
};

class VIP : public person
{
public:
    // здесь можно добавить ещё методов

protected:
    class data; // класс VIP::data - наследник класса person::data
};

В результате можно выполнить такой код:

person president = VIP("mr. President");

Напоминает высокоуровневый код, хотя это банальный конструктор копирования, просто по ссылке на предка передан объект класса-наследника.
Это довольно удобно, когда вся работа со ссылками, их хранением, копированием содержимого по данной ссылке занимается реализация библиотеки, а пользователь просто использует классы библиотеки в своё удовольствие. Когда не нужно заботиться о генерируемых конструкторах и операторах и наследование помогает, а не мешает - это же просто чудо!

Послесловие


Создавайте самое удобное API, удивляйте приятно, пусть каждый пользователь вашей библиотеки приводит её в пример всем и каждому в восторге от её использования.
Может вы с этого ничего и не получите, вероятно проще было бы наваять тяп-ляп-API, да и быстрее будет, но этого ли вы хотите от разработки библиотеки предназначенной таким же как и вы разработчикам?
Конечно нет! Разработка - это не просто работа, это - удовольствие от самосовершенствования, искусство познавать и умение применять знания с толком и по делу. Ведь так приятно, когда твоя работа приносит удовольствие не только тебе, но и многим людям вокруг!

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


Копирование при изменении (Copy-on-write).
Двойная диспетчеризация (Double dispatch)
Проект с кодом приводящимся в статье выложен сюда - скачивайте, пробуйте, экспериментируйте.
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 71

    –5
    если пишите API то про классы и slt/boost надо забыть. только struct и POD типы
      +11
      Чем вам STL не угодил? Он же входит в стандарт C++.
        +3
        Возможно речь шла о том, что у C++ Standard Library нет (пока) стабильного ABI. Но как-то уж очень категорично
        А про аббревиатуру POD стоит уже забыть, как тут уже несколько раз заметили, 2014 на дворе.
          –1
          Ну давайте следить за тем, что и как поддерживают gcc и MS; поддерживать редкими патчами то, что в других компиляторах не работает. Зачем забывать-то?
            0
            Затем, что они свято уверены, что ничего, кроме MSVC в природе нету.

            Раз Microsoft сказал не использовать STL, а предложил использовать COM — значит все переходим на COM. И то, что на всех остальных платформах этой проблемы нету (даже на Windows при использовании других компиляторов типа MinGW!) никого не волнует. «Сказали в сад — значит в сад».
              0
              Все бы хорошо, вот только половина библиотек и продуктов, в том числе open source не хочет собираться mingw. Под виндой им подавай MSVС, или развлекайся с компиляцией.
                +1
                Если вам действительно нужны все эти библиотеки — тогда да, вопросов нету, но на Winows свет клином не сошёлся, так что во многих случаях можно без поддержки MSVC обойтись. Или использовать для сборки всех библиотек одну конкретную версию MSVC и разделяемую стандартную библиотеку — в этом случае тоже всё работает.
                  0
                  Если вы пишете server-side, то про винду и вообще кроссплатформенность можно и не упоминать. Ваш продукт будет работать на вашем кластере, который вы полностью контролируете. Если речь заходит о клиентском приложении, то с этой проблемой вы, к сожалению, рано или поздно столкнетесь.
                    0
                    Можно поставлять клиентам библиотеки, скомпилированные несколькими версиями компилятора, например VS 2010, VS 2012 и VS 2013. Это может уменьшить, а чаще свести к нулю, проблемы из-за незнания компиляторами от Microsoft понятия совместимость ABI.
            +1
            Неформально в юникс системах для плюсов принято использовать itanium c++ abi.
          0
          заверните API в dll и вызовите в другом языке. Да что там — в другой версии компилятора — здравствуйте эксепшены и утечки памяти
            0
            Для plain C есть специальная техника работы через handle объекта (указатель на forward declared struct, у которой единственный член — объект оборачиваемого класса, сама структура описывается в .cpp, где уже можно использовать #include C++ классов) и дублирование методов экспортируемыми extern «C» функциями. В целом в других языках есть удобные биндинги: JNI, MC++, Boost.Python, Rice и т.п., но если совсем край и надо чистый Си, то нужно для Си делать API отдельно в рамках wrapping-процесса, тут уж придётся делать downgrade типов.
              0
              --то нужно для Си делать API отдельно в рамках wrapping-процесса, тут уж придётся делать downgrade типов

              в реальной жизни api делают и ставляют на года. и потом сертифицируют и запрещают менять.

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

              И около 3 лет нам не давали новее версии. Потому что при смене версии надо снова сертифицировать весь софт.

              Я сменил три версии Visual Studio — представте что бы было будь там STL наружу. Да и никто бы не позволил.
          +18
          И как там в каменном веке поживается?
            0
            Ну без классов умных указателей, потоков с мьютексами, без стандартных контейнеров массивов, множества, маппинга, да даже без всем известного базового исключения будет как-то трудно разрабатывать понятную библиотеку. К тому же что будем кидать в виде исключения, throw 1, если только struct и POD-типы? Для языка Си можно сделать отдельный интерфейс, обернув данные в структуры с forward declaration в .h (с обычным вложением объекта как единственного поля в структуре в .cpp-файле) и продублировав методы аналогичными extern «C» функциями, но это исключительно для случаев когда нужен чистый Си и для него отдельно выносится API. Я понимаю что любой STL класс можно реализовать самому, но стандартные реализации довольно хороши и всем известны, STL есть везде, чем вам не нравится использование std:: классов?
              0
              --К тому же что будем кидать в виде исключения, throw 1, если только struct и POD-типы?

              евенты: с таким функтором можно делать логгирование и даже прогресс бар в приложении

              typedef ptrdiff_t (*pfnMsg)( int logLevel, int barNumber, int resourceId, int position
              , const wchar_t *wzMessage, const char *szFuncName, const int lineNumber, ptrdiff_t pCustomObject );
                +2
                Хм… вы там что-то про POD'ы и структуры рассказывали и тутже предлагаете использовать wchar_t: тип, переносимость которого между разными компиляторами не гарантируется…
              +4
              Я так понимаю, речь шла про «показ наружу». Внутри то, конечно, можно использовать все, что угодно (slt/boost), а снаружи должны быть видны только pod'ы. Не понимаю почему человека заминусовали.
                0
                Это, наверно, правильный подход — когда наружу торчат интерфейся только из POD'ов и структур. Но как показывает практика, при таком подходе к архитектуре интерфейса получается так, что пользователь использует удобные ему фичи(да хоть тотже STL с его контейнерами, смарт-пойнтерами и тд и тп), а потом ему надо вызвать что-то из вашей библиотеки и он начинает говнокодить, реализуя прослойку между своим приложением и вашей библиотекой. И хорошо, если можно преобразовать типы на той же памяти(например, дернув сырой указатель из std::vector), а то может потребоваться перепаковывать память(например, приводить std::list к void*) и просядет производительность приложения.
                  +1
                  Ну тот же std::string вполне можно вернуть, не возвращать же const char*. Или с std::vector или std::deque работать, не принимать же странный element* с количеством отдельно. Совсем уж до POD-типов зачем себя ограничивать. Тот же std::map или std::set замучаешься сам реализовывать. Вряд ли кто-то в свободное время балуется реализацией красно-чёрных деревьев или например тот же std::unordered_map, его вообще проблематично заменить.
                    0
                    О том и речь, что даже std::string нельзя возвращать, если библиотека распространяется в скомпилированном виде и претендует на совместимость.

                    Представьте, что у вас (под Windows) есть динамическая библиотека, возвращающая ссылку на экземпляр такого типа:
                    struct {
                    std::string get_string() const { return «abc»; }
                    }
                    Допустим, библиотека скомпилирована с помощью msvc++.
                    При использовании библиотеки в программе, собранной другим компилятором, получаем undefined behaviour (почти наверняка, поскольку внутреннее представление строки может быть другим).
                    Причина — отсутствие наперед определенного стабильного ABI у std::string.
                      +1
                      Причина — отсутствие наперед определенного стабильного ABI у std::string.

                      Стабильный ABI отсутствует не у std::string, а у компиляторов msvc++ и стандартной библиотеки из msvc++. Не надо переносить проблемы msvc++ на другие компиляторы, где стабильный ABI существует уже много лет.

                      Для msvc++, возможным решением будет поставка сборок библиотек сразу под несколько версий msvc++, все равно другие компиляторы на платформе Windows распространены очень мало.
                        0
                        Как сказать, интеловый компилятор довольно популярен под Windows.
                          0
                          У него нет своего рантайма С++ и он использует рантайм MSVC++, а также бинарно совместим с MSVC.
                  +2
                  Для API классы и STL/Boost — это совершенно нормально.

                  Проблемы начинаются с ABI. То есть там, где клиентом библиотеки могут быть не плюсы.
                    0
                    Не обязательно другие языки, другой компилятор, другая версия того же компилятора, да даже другой набор флагов компилятора могут дать бинарно-несовместимые результаты.
                      0
                      Ну в принципе да. Я как-то привык уже думать о плюсовых библиотеках как о коде, который собирается под данный конкретный компилятор, и линкуется статически (все равно там обычно куча шаблонов в заголовках, а то и все).
                  +7
                  данные можно объявить отдельным классом без реализации в заголовочном файле и вынести его целиком в .cpp-файл с реализацией методов

                  Думаю, стоит упомянуть что такой паттерн называется Pimpl.
                    0
                    Не совсем, речь идёт о разделении данных и интерфейса. К тому же мне не нравится подход описанный как Pimpl (указатель на реализацию), если говорить о том, что в С++ в .cpp выносится имплементация, то она и так выносится в .cpp-файл, для того и нужно разделение .h/.cpp. Подход переноса описания данных в реализацию и разделения именно данных от интерфейса класса мне ближе, он берёт своё начало в старом-добром языке Си, когда структура описывалась forward declatation и использовалась как handle сродни this в С++, т.е. передавалась первым параметром в функции, который работали на манер методов в С++. Для примера можно посмотреть API WinPCAP… Как видите я везде избегаю нелюбимого мной понятия impl и данные класса описал именно как данные: class data. Да и copy_on_write шаблон интуитивно понятно что он работает с данными класса, а не с его реализацией, копируется же не реализация, а данные объекта.
                      +5
                      Если быть точнее. то это пример технологии D-Pointer, который конечно же является частным случаем Pimpl.
                      +1
                      Мне кажется, это выступление Sean Parent подходит по общему духу к статье
                      channel9.msdn.com/Events/GoingNative/2013/Inheritance-Is-The-Base-Class-of-Evil

                      Кстати, есть вот такая — довольно интересная — книжка: API Design for C++
                        0
                        С++ уже низкоуровневый… эххх, снова преамбула холиварная.
                          –3
                          Зарекался не критиковать никогда авторов статей но тут не смог удержаться.

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

                          Для нуба статья бесполезная свершенно — он не поймет ничего.
                          Для середничка ненужная — он не пишет свои мегалибы и свои движки а пользуется готовым.
                          Для продвинутого прогера тут всё #баян и #спасибоКЭП
                            +6
                            Для середнячка как раз статья нужная, поскольку часто они работают в команде при разработке серьёзной либы, а иногда пишут что-то своё в порыве альтруизма на волне своих локальных успехов. Уж извините, что не угодил продвинутому «прогеру», но для PRO-уровня писать дело неблагодарное, аудитория узкая и перманентно враждебно настроенная. В любом случае спасибо что снизошли до уровня убогого автора, пишущего никчёмные статьи. Я старался как мог сделать статью и понятно простой и полезной широкой аудитории. Увы мне, если не вышло.
                              –1
                              Вы зря ёрничаете. В убогости слога и уж тем более в никчемности я вас не обвинял и даже мысли такой не было.
                              Более того, статья написана хорошим словом и никаких нареканий именно к содержанию нет.

                              Вопрос был чисто риторический: «а нафига фсё это?»
                              Извините пожалуйста если из моих слов могло показаться что я вас хочу оскорбить или обидеть.

                              Я старался как мог сделать статью и понятно простой и полезной широкой аудитории.

                              Широкой, это, простите, вы польстили себе сейчас. :-)
                                0
                                Ну лет 5-7 назад мне бы такая статья не помешала бы.
                                  0
                                  7 лет назад и деревья были выше и небо голубее и трава зеленее

                                  эхххх
                              0
                              Вот вы и сами сказали, что среднячки этого не знают, а про — знают. Соответственно, статья нужна тем среднякам, которые в будущем вырастут в про.
                              +1
                              Хотелось бы добавить, что в Qt для вышеописанного есть удобные готовые средства. Они используются в самом коде Qt и могут использоваться при написании своих библиотек. См. d-pointer, QSharedDataPointer.
                                0
                                Поддерживаю, Qt отличная библиотека не только сама по себе, но и как набор инструментов для написания своих библиотек. Но увы, иногда завязка на Qt бывает избыточной.
                                0
                                По поводу самой статьи.
                                Использовать shared_ptr во всех класс библиотеки, если она будет применяться только в проектах на C++ — ошибка
                                1) Слишком дорогое удовольствие.
                                2) Непривычное поведение, т.к. в стандартном случае передается указатель, если программист хочет «расшарить» данные

                                А так, хотите писать хорошой API — берите соглашения, принятые в Qt и следуйте им.
                                  0
                                  Я тоже сперва так думал, позже оказалось что я экономил на спичках, так что std::shared_ptr можно спокойно копировать, в большинстве случаев 99% времени съедают сетевое взаимодействие (с той же БД или удалёнными серверами с бизнес-логикой по RPC) либо какие-нибудь адовые алгоритмы коллег по цеху. В C++ не хватает кстати асинхронности и транзакционности данных, но в целом при должном умении на C++ можно создать систему любой сложности с безграничными возможностями для оптимизации. Что до частых выделений памяти и её фрагментации, то с этим можно бороться выделяя общие сущности, размещая в них более мелкие, так например для полей выборки в каждой записи можно через placement new разместить все данные результата запроса, это сэкономит время на выделение на порядок меньшего числа памяти, но в целом сами записи в выборке видимо придётся хранить именно по ссылке на данные через shared_ptr или copy_on_write, просто потому что на бизнес-логике часто идёт дообработка выборки с базы данных. Это по первому пункту.
                                  По второму: поведение ничем не отличается от стандартного, разве что копирование произойдёт позже. Заставлять разработчика оперировать указателями, имхо плохая практика, если это обычные указатели, любые операции с ними небезопасны, если это указатели умные, то операции с ними громоздки и лучше всю дополнительную работу с умными указателями сразу завернуть в класс, с которым пользователь будет работать уже без ошибок.
                                    +2
                                    Если
                                    в большинстве случаев 99% времени съедают сетевое взаимодействие (с той же БД или удалёнными серверами с бизнес-логикой по RPC) либо какие-нибудь адовые алгоритмы коллег по цеху
                                    , то зачем вы вообще на C++ пишете? Чтоб помучаться?
                                      0
                                      Потому что нужна максимальная скорость на критичных участках. Но это не отменяет того факта, что пользователю системы нужно удобное API, вне зависимости от того, что твориться в движке. Это как автомобиль. Машина должна быть красивой, хорошо ездить, удобно управляться и не должна требовать каких-то навыков сверх навыка вождения.
                                        +1
                                        Ну так и написать тогда на C++ только критичные участки, а остальное на том же питоне, например. И API На питоне в том числе.
                                          0
                                          Стоп-стоп-стоп, что это за критичные участки, которые занимают 1% от выполнения?
                                          Давайте сначала разберемся с этим, а потом пойдем дальше.

                                          Ну и да, если мы не используем обычные указатели нафига гам С++? Ведь тем самым мы убили практически все преимущества плюсов мигом.
                                          Удаление данных из кучи происходит не тогда, когда мы этого ожидаем, лишаемся всех преимуществ адресной арифметики — мощнейшего инструмента, лишаемся возможностей сделать reinterept_cast, лишаемся части оптимизаций. Что осталось? Остались возможности деструкторов, шаблоны и макросы. Все.

                                          Ну а в довесок всего этого приходится выдумывать свой сборщик мусора и свои средства управления памяти. Эти велосипеды вам точно нужны?
                                          Может проще заморочиться со статистической оптимизацией в C# и Java?

                                            +2
                                            У C++ есть определённые преимущества в при разработке системного кода.
                                            — Идиома RAII, обеспечивающая детерминированное управление любыми ресурсами (не только памяти).
                                            — Шаблоны, обеспечивающие zero-cost абстракции.
                                            — Быстрый старт приложения.
                                            — Как правило, существенно меньшее потребление памяти.
                                            — Возможность использовать низкоуровневые платформо-зависимые примитивы (к счастью, обёртки над ними постепенно проникают в «высокоуровневые» языки).

                                            > если мы не используем обычные указатели нафига гам С++?
                                            Использовать сырые указатели обычно нет необходимости. Часто больше подходят итераторы и умные указатели.

                                            > Стоп-стоп-стоп, что это за критичные участки, которые занимают 1% от выполнения?
                                            Может, 1% и обуслен реализацией на соответствующем языке? Возможно, при реализации на Python соотношение было бы совсем другим.
                                              0
                                              >У C++ есть определённые преимущества в при разработке системного кода.
                                              Собственно я про первые два пункта и сказал. Кстати, повсеместное использование shared_ptr вставляет палки в детерминированность.
                                              >Быстрый старт приложения.
                                              Это точно про API для высокровневых языков?


                                              >Использовать сырые указатели обычно нет необходимости. Часто больше подходят итераторы и умные указатели.
                                              Итераторы это хорошо, но итераторы есть и в других более высокуровневых язык. А, напомню, приложение, уже написано не на C++.

                                              >Может, 1% и обуслен реализацией на соответствующем языке? Возможно, при реализации на Python соотношение было бы совсем другим.
                                              Возможно, но ЧТО?
                                                +1
                                                > использование shared_ptr вставляет палки в детерминированность

                                                Абсолютно верно, глубокие деревья объектов могут приводить к задержкам при освобождении ресурсов. Их стоит рассматривать как зло, иногда необходимое. Sean Parent вообще считает std::shared_ptr<T> современным аналогом глобальных переменных.
                                                COW лично я тоже не особо приветствую, да и дизайн Qt весьма далёк от совершенства.

                                                > Это точно про API для высокровневых языков?
                                                Не понял мысли. Речь вроде шла о том, зачем связываться с C++ без сырых указателей, если есть «высокоуровневые» языки со сборщиком мусора.

                                                > итераторы есть и в других более высокуровневых язык.
                                                Там совсем другие итераторы, они проще, но тяжеловесней. В C++ алгоритмы на итераторах превращаются в компактные и быстрые циклы без виртуальных вызовов. По сути это тонкая прослойка, которую компилятор отбрасывает, превращая в сырые указатели.

                                                Кстати, ещё один весомый довод в пользу системного программирования на C++:
                                                — Отсутствие серьёзных задержек при сборке мусора, меньшая latency.

                                                Ну и C++11 — гораздо более приятный для повседневного использования язык, чем был C++98. Комитет развивает сильные стороны языка в правильном направлении, не ломая при этом обратной совместимости. Это не может не радовать.
                                                  0
                                                  >Речь вроде шла о том, зачем связываться с C++ без сырых указателей, если есть «высокоуровневые» языки со сборщиком мусора.
                                                  Ну у нас статья об API. И ситуация такова: народ уже пишет прогу на Java, Python и т.д, но часть пишется на плюсах.

                                                  Так вот, если мы выкинули из употребления сырые указатели, перешли на shared_ptr, то ради чего мы вносим в уже реализующийся проект доп.сложности? Стоит ли оно того?
                                                    +1
                                                    народ уже пишет прогу на Java, Python и т.д, но часть пишется на плюсах. Стоит ли оно того?

                                                    Автор работает в области GameDev, я думаю, C++ там — весьма разумный выбор.

                                                    выкинули из употребления сырые указатели

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

                                                    перешли на shared_ptr

                                                    Вовсе не обязательно переходить на shared_ptr. Да и не обязательно вообще начинать с сырых указателей. C++ наиболее логичен и производителен в тех случаях, когда мы используем value-семантику, которой буквально пропитан дизайн языка. А если где-то удобно воспользоваться shared_ptr — почему бы и нет.
                                                      0
                                                      Еще раз, я не спрашиваю «зачем вообще писать на C++?» (более того, сейчас я пытаюсь убедить коллег перейти с C# на плюсы), я спрашиваю автора «зачем выбирать С++, когда вы уже пишете проект на другом языке, при этом вы делаете это для мест, которые занимают 1% от общего времени исполнения, при этом вы отказываетесь от сырых указателей и всю работу с умными обернули в классы( тем самым лишили себя львиной доли оптимизаций и возможностей), перевели все классы API на shared_ptr (и убили возможность детерминированного управления ресурсами)?»

                                                      >>Да и не обязательно вообще начинать с сырых указателей. C++ наиболее логичен и производителен в тех случаях, когда мы используем value-семантику, которой буквально пропитан дизайн языка.
                                                      Да, да, да и еще раз да. В C# меня просто бесит, что я не могу передать по значению или хотя бы по константной ссылке. Невозможность отследить эти моменты заставляет писать корявый и медленный код
                                                        +1
                                                        Я пишу на C++, всегда на нём писал, не считая десяток других языков (включая C#) которыми я пользовался для решения как правило локальных специфических задач (типа поправить скрипты сборки на Perl или обернуть сборку C++ через MC++ для C#/VB.NET). Проекты мы всегда писали на C++, но некоторое время назад понадобилась скриптовая обвязка. Всё отлично работает в C++ без shared_ptr, да и с ним всё замечательно работает. C++ тем и хорош, что нет никаких догм разработки, периодически выходит очередная идиотская книга, где всех учат как «правильно» программирость на C++, после чего по компании проносится очередная эпидемия каких-то немыслимых private-virtual методов или фабрик, которые создают фабрики. В целом C++ нас вполне устраивал, но некоторые вещи лучше заскриптовать, проверить и иметь возможность их быстро править. Что-то совсем прикладное. Здесь нам помог Python и связка Boost.Python. Не везде требуется сетевое взаимодействие, там где оно не нужно, преимущества реактивного исполенения кода написанного на C++ ни с чем не сравнимо.
                                                        Ссылки, особенно константные — это здорово, как и размещение на стеке, управление памятью через размещающий new. В общем C++ особо и не выбирали, мы на нём просто без вариантов пишем. Python просто для прикладников иногда очень удобен.
                                                          0
                                                          Вот теперь понятно, ситуация ровно наоборот, кою я видел после прочтения. Может это стоит отразить в статье?
                                                            0
                                                            Ну первый абзац начинается с того, что мы пишем Платформу на C++ для использования его в прикладных проектах на C++ и Python.
                                                        0
                                                        Не работаю я в GameDev (к сожалению, в Ярославле серьёзная разработка GameDev отсутствует).
                                                        Указатели в API когда торчат наружу — это всегда плохо. Указатель по определению может быть nullptr, а значит мы заставляем программиста проверять результат, чего он разумеется может и не сделать.
                                                        Просто shared_ptr удобен для примера, на котором я объяснял суть copy-on-write подхода. В принципе для однотипных или однородных объектов есть возможность например использовать placement new используя память объекта-владельца (например запись выборки и поля этой записи).
                                                          0
                                                          Не работаю я в GameDev

                                                          Извините, виноват, gamedev у вас указан интересах. Поторопился и сделал неправильные выводы.
                                      0
                                      Я когда пишу на C++ обычно пишу интерфейс класса в header, и наследуюсь от этого описания в файле реализации (ну и +статический метод create который создает соответствующий объект). Получается немного сложнее но при этом более логично (для меня) и так-же безопасно. Тут конечно могут возникнуть проблемы с производительностью из-за виртуальных методов, но по факту это очень редко является узким местом. Ну и в реализуемые методы становятся чище. Конструкция типа children_.do_something() для меня куда более интуитивно понятна чем data_.children_.do_something()
                                        +7
                                        Да вы пытаетесь бороться с С++ и сделать из него ваш питон. Ничего хорошего при этом не выйдет. Все ваши решения — это ужасное переусложение, вызванное отчасти и тем, что вы не хотите использовать подход С++.

                                        Например, вы недовольны тем как работает копирование, на самом же деле все несколько проще — если вы хотите копировать — то копируйте. А если не хотите копировать, то сохраняйте ссылки/указатели. И боже упаси вас когда-нибудь использовать COW.
                                          +2
                                          Да ну что вы, я очень люблю C++ и замечательно умею его готовить. «Ужасное переусложнение» никто не увидит, если не заглянет в код реализации. Что до Copy-on-write, то этот подход в том же g++ применяется для std::string(!) что неплохо оптимизирует всевозможную работу с текстом. Почти везде CoW используется в Qt, тот же QString не копирует потроха с текстом при копировании объекта. По-вашему у Qt плохая архитектура?
                                            0
                                            Ваше переусложение пренепременно сломается сразу после того, как вы уйдете из проекта и какому-то несчастному-таки придется копаться в реализации.

                                            Очень сомневаюсь, что g++ использует COW для std::string, потому что это было бы существленным отступлением от стандарта. Подробнее здесь.

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

                                              +1
                                              Тем не менее реализация GCC std::string сделана на основе copy-on-write, за что мы и любим GCC, можно по сорцам увидеть ref_count. Насчёт стандарта Вы правы, всё верно, не должно быть CoW в std::string, тем не менее данные шарятся между несколькими строками. Завязываться на это нельзя разумеется, потому что например реализация STL от Microsoft сделана по стандарту с честным копированием. Для любителей гарантированного CoW с возможностью декодирования и прочими плюшками есть QString. Если честно, решительно не понимаю за что Вы так не взлюбили copy-on-write подход.
                                                0
                                                Да вам уже написали ниже. Суть в том, что вместо того, чтобы один раз скопировать, вам приходится все время проверять, не нужно ли это сделать сейчас. В многопоточном приложении это означает доступ к атомарной переменной, со всеми вытекающими последствиями вроде синхронизации памяти.

                                                К тому же, если делать COW самому, то вы, скорее всего, накосячите.
                                                  +2
                                                  Реализация в gcc сделана еще в те времена, когда это соответствовало стандарту (C++03). А теперь они не могут её изменить, потому что тогда поломается их ABI (это, кстати, к вопросу о стабильном C++ ABI...). Тот же Clang, например, поменял.
                                                    0
                                                    На самом деле до С++11 COW было отчасти необходимо, потому что не было механизма move. Теперь же вы можете использовать move, если хотите передать владение, ссылки и указатели, если хотите совместного владения, и копирование, если вы действительно хотите копию.
                                                +2
                                                Ну вообще-то CoW для строк общепризнан плохим решением. Одна из главных причин — плохая производительнось во многопоточной среде (потому что счётчики ссылок должны быть синхронизированы). Кстати, в новом стандарте C++11 CoW для строк больше не легален.
                                                  0
                                                  Ну статья от 99-го года, а CoW жив и по сей день. Массовое копирование при многопоточном доступе также будет причиной плохой производительности. Безопаснее вернуть std::string вместо ссылки на него, опять же как это сказано в статье, это освобождает от обязательств в реализации и завязки на хранение именно std::string. Если нет CoW, то при копировании из поля класса в значение результата будет довольно дорогое копирование, сложность которого невозможно померять, так как строки произвольной длины. Есть всевозможные оптимизации строк, именно по причине дорогого копирования, но как раз в данном случае мы получим проседание по производительности при честном копировании строк.
                                                    0
                                                    Ну статья от 99-го года, а CoW жив и по сей день.
                                                    CoW жив и по сей день потому что совместимость. C++11-совместимая версия была добавлена в GCC восемь лет назад, но стандартизация несколько затянулась и потому «большой слом» всё ещё не случился.

                                                    Есть всевозможные оптимизации строк, именно по причине дорогого копирования, но как раз в данном случае мы получим проседание по производительности при честном копировании строк.
                                                    Прогоните ваши тесты, очень может быть, что и в вашем случае (как и у нас) CoW версия будет медленнее. Часто — заметно медленнее.
                                                    0
                                                    (потому что счётчики ссылок должны быть синхронизированы).

                                                    Эээ — atomic?
                                                    Вобще с 99го года очень много воды утекло.
                                                      +1
                                                      Ну атомики тоже очень медленны. (И одновременно очень быстры по сравнению с мьютексами)

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

                                              Самое читаемое