Про бідного C + + API змалювати слівце!

    Бажання написати про 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 + +). Суть методу Cow в наступному, поки об'єкт передається в методи як 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 <vector>
, як загалом і сам додатковий клас при використанні класу person ззовні. В принципі і сам std :: string можна змінити на forward declaration , тоді в заголовному файлі можна уникнути ще і
#include <string>
, ще більше прискоривши компіляцію при використанні вашого класу. Зробити це можна оголошенням такого виду:
 
 
// можно вынести все эти объявления в файл вида <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)
Проект з кодом приводиться в статті викладений сюди — завантажуйте, пробуйте, експериментуйте.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.