какой класс реализует базовую функциональность интерфейсных элементов

Урок №168. Чистые виртуальные функции, Интерфейсы и Абстрактные классы

Обновл. 15 Сен 2021 |

На этом уроке мы рассмотрим чистые виртуальные функции, интерфейсы и абстрактные классы.

Абстрактные функции и классы

До этого момента мы записывали определения всех наших виртуальных функций. Однако C++ позволяет создавать особый вид виртуальных функций, так называемых чистых виртуальных функций (или «абстрактных функций»), которые вообще не имеют определения! Переопределяют их дочерние классы.

Таким образом, мы сообщаем компилятору: «Реализацией этой функции займутся дочерние классы».

Использование чистой виртуальной функции имеет два основных последствия. Во-первых, любой класс с одной и более чистыми виртуальными функциями становится абстрактным классом, объекты которого создавать нельзя! Подумайте, что произойдет, если мы создадим объект класса Parent:

Во-вторых, все дочерние классы абстрактного родительского класса должны переопределять все чистые виртуальные функции, в противном случае — они также будут считаться абстрактными классами.

Пример чистой виртуальной функции

Рассмотрим пример чистой виртуальной функции на практике. На одном из предыдущих уроков мы создавали родительский класс Animal и дочерние классы Cat и Dog:

Мы запретили создавать объекты класса Animal, сделав конструктор protected. Однако, остаются две проблемы:

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

По-прежнему могут быть дочерние классы, которые не переопределяют метод speak().

Результат выполнения программы:

Что случилось? Мы забыли переопределить метод speak(), поэтому lion.Speak() вызвал Animal.speak() и получили то, что получили.

Решение — использовать чистую виртуальную функцию:

Здесь есть несколько вещей, на которые следует обратить внимание. Во-первых, speak() теперь является чистой виртуальной функцией. Это означает, что Animal теперь абстрактный родительский класс, и нам уже не нужен спецификатор protected (хотя он и не будет лишним). Во-вторых, поскольку наш класс Lion является дочерним классу Animal, но мы не определили Lion::speak(), то Lion считается также абстрактным классом. Поэтому, если мы попытаемся скомпилировать следующий код:

То получим ошибку, сообщающую о том, что Lion является абстрактным классом, а создавать объекты абстрактного класса нельзя. Из этого можно сделать вывод, что для того, чтобы создать объект класса Lion, нам нужно переопределить метод speak():

Теперь уже другое дело:

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

Чистые виртуальные функции с определениями

Оказывается, мы можем определить чистые виртуальные функции:

В этом случае speak() по-прежнему считается чистой виртуальной функцией (хотя позже мы её определили), а Animal по-прежнему считается абстрактным родительским классом (и, следовательно, объекты этого класса не могут быть созданы). Любой класс, который наследует класс Animal, должен переопределить метод speak() или он также будет считаться абстрактным классом.

При определении чистой виртуальной функции, её тело (определение) должно быть записано отдельно (не встроено).

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

Результат выполнения программы:

Интерфейсы

Интерфейс — это класс, который не имеет переменных-членов и все методы которого являются чистыми виртуальными функциями! Интерфейсы еще называют «классами-интерфейсами» или «интерфейсными классами».

Интерфейсные классы принято называть с I в начале, например:

IErrorLog ( ) < >; // создаем виртуальный деструктор, чтобы вызывался соответствующий деструктор дочернего класса в случае, если удалим указатель на IErrorLog

Любой класс, который наследует IErrorLog, должен предоставить свою реализацию всех 3 методов класса IErrorLog. Вы можете создать дочерний класс с именем FileErrorLog, где openLog() открывает файл на диске, closeLog() — закрывает файл, а writeError() — записывает сообщение в файл. Вы можете создать еще один дочерний класс с именем ScreenErrorLog, где openLog() и closeLog() ничего не делают, а writeError() выводит сообщение во всплывающем окне.

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

Намного лучшим вариантом будет реализация через IErrorLog:

Теперь пользователь через передачу объектов может определить самостоятельно, какой класс следует вызывать. Если он хочет, чтобы ошибка была записана в файле, то он передаст в функцию mySqrt() объект класса FileErrorLog. Если он хочет, чтобы ошибка выводилась на экран, то он передаст объект класса ScreenErrorLog. Или, если он хочет сделать то, что вы не предусмотрели, например, отправить кому-то Email-ом сообщение ошибки, то он может создать новый дочерний класс EmailErrorLog, который будет наследовать IErrorLog, и передавать объект этого класса! Таким образом, реализация через IErrorLog делает нашу функцию более гибкой и независимой.

Не забудьте о подключении виртуальных деструкторов в ваши интерфейсные классы, чтобы при удалении указателя на интерфейс вызывался деструктор соответствующего (дочернего) класса.

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

Чистые виртуальные функции и виртуальная таблица

Абстрактные классы имеют виртуальные таблицы, которые могут использоваться, если у вас есть указатель или ссылка на абстрактный класс. Запись чистой виртуальной функции в виртуальной таблице обычно содержит либо нулевой указатель, либо указывает на общую функцию, которая выводит ошибку (иногда эта функция называется __purecall ), если не было обнаружено переопределения.

Читайте также:  что делать если залил ухо водой и заложила

Чем отличается абстрактный класс от интерфейса в языке C++?

Ответ

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

Поделиться в социальных сетях:

Источник

Разработка интерфейсных классов на С++

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

Оглавление

Введение

Интерфейсным классом называется класс, не имеющий данных и состоящий в основном из чисто виртуальных функций. Такое решение позволяет полностью отделить реализацию от интерфейса — клиент использует интерфейсный класс, — в другом месте создается производный класс, в котором переопределяются чисто виртуальные функции и определяется функция-фабрика. Детали реализации полностью скрыты от клиента. Таким образом реализуется истинная инкапсуляция, невозможная при использовании обычного класса. Про интерфейсные классы можно почитать у Скотта Мейерса [Meyers2]. Интерфейсные классы также называют классами-протоколами.

Использование интерфейсных классов позволяет ослабить зависимости между разными частями проекта, что упрощает командную разработку, снижается время компиляции/сборки. Интерфейсные классы делают более простой реализацию гибких, динамических решений, когда модули подгружаются выборочно во время исполнения. Использование интерфейсных классов в качестве интерфейса (API) библиотек (SDK) упрощает решение проблем двоичной совместимости.

Интерфейсные классы используются достаточно широко, с их помощью реализуют интерфейс (API) библиотек (SDK), интерфейс подключаемых модулей (plugin’ов) и многое другое. Многие паттерны Банды Четырех [GoF] естественным образом реализуются с помощью интерфейсных классов. К интерфейсным классам можно отнести COM-интерфейсы. Но, к сожалению, при реализации решений на основе интерфейсныx классов часто допускаются ошибки. Попробуем навести ясность в этом вопросе.

1. Специальные функции-члены, создание и удаление объектов

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

1.1. Специальные функции-члены

Если программист не определил функции-члены класса из следующего списка — конструктор по умолчанию, копирующий конструктор, оператор копирующего присваивания, деструктор, — то компилятор может сделать это за него. С++11 добавил к этому списку перемещающий конструктор и оператор перемещающего присваивания. Эти функции-члены называются специальные функции-члены. Они генерируются, только если они используются, и выполняются дополнительные условия, специфичные для каждой функции. Обратим внимание, на то, что это использование может оказаться достаточно скрытым (например, при реализации наследования). Если требуемая функция не может быть сгенерирована, выдается ошибка. (За исключением перемещающих операций, они заменяются на копирующие.) Генерируемые компилятором функции-члены являются открытыми и встраиваемыми.

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

Подробности о специальных функциях-членах можно найти в [Meyers3].

1.2. Создание и удаление объектов — основные подробности

Создание и удаление объектов с помощью операторов new/delete — это типичная операция «два в одном». При вызове new сначала выделяется память для объекта. Если выделение прошло успешно, то вызывается конструктор. Если конструктор выбрасывает исключение, то выделенная память освобождается. При вызове оператора delete все происходит в обратном порядке: сначала вызывается деструктор, потом освобождается память. Деструктор не должен выбрасывать исключений.

Если оператор new используется для создания массива объектов, то сначала выделяется память для всего массива. Если выделение прошло успешно, то вызывается конструктор по умолчанию для каждого элемента массива начиная с нулевого. Если какой-нибудь конструктор выбрасывает исключение, то для всех созданных элементов массива вызывается деструктор в порядке, обратном вызову конструктора, затем выделенная память освобождается. Для удаления массива надо вызвать оператор delete[] (называется оператор delete для массивов), при этом для всех элементов массива вызывается деструктор в порядке, обратном вызову конструктора, затем выделенная память освобождается.

Внимание! Необходимо вызывать правильную форму оператора delete в зависимости от того, удаляется одиночный объект или массив. Это правило надо соблюдать неукоснительно, иначе можно получить неопределенное поведение, то есть может случиться все, что угодно: утечки памяти, аварийное завершение и т.д. Подробнее см. [Meyers2].

Любую форму оператора delete безопасно применять к нулевому указателю.

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

1.3. Уровень доступа деструктора

1.4. Создание и удаление в одном модуле

Если оператор new создал объект, то вызов оператора delete для его удаления должен быть в том же модуле. Образно говоря, «положи туда, где взял». Это правило хорошо известно, см., например [Sutter/Alexandrescu]. При нарушении этого правила может произойти «нестыковка» функций выделения и освобождения памяти, что, как правило, приводит к аварийному завершению программы.

1.5. Полиморфное удаление

1.6. Удаление при неполном объявлении класса

warning C4150: deletion of pointer to incomplete type ‘X’; no destructor called

Ситуация эта не надумана, она легко может возникнуть при использовании классов типа интеллектуального указателя или классов-дескрипторов. Скотт Мейерс разбирается с этой проблемой в [Meyers3].

2. Чисто виртуальные функции и абстрактные классы

Концепция интерфейсных классов базируется на таких понятиях С++ как чисто виртуальные функции и абстрактные классы.

2.1. Чисто виртуальные функции

В отличии от обычной виртуальной функции, чисто виртуальную функцию можно не определять (за исключением деструктора, см. раздел 2.3), но она должна быть переопределена в одном из производных классов.

Читайте также:  с каким цветом ассоциируется спорт

Чисто виртуальные функции могут быть определены. Герб Саттер предлагает несколько полезных применений для этой возможности [Shutter].

2.2. Абстрактные классы

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

2.3. Чисто виртуальный деструктор

В ряде случаев чисто виртуальным целесообразно сделать деструктор. Но такое решение имеет две особенности.

Пример использования чисто виртуального деструктора можно найти в разделе 4.4.

3. Интерфейсные классы

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

3.1. Реализации

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

Ключевым моментом концепции интерфейсных классов является полное отделение интерфейса от реализации — клиент работает только с интерфейсным классом, реализация ему не доступна.

3.2. Создание объекта

Недоступность класса реализации вызывает определенные проблемы при создании объектов. Клиент должен создать экземпляр класса реализации и получить указатель на интерфейсный класс, через который и будет осуществляться доступ к объекту. Так как класс реализации не доступен, то использовать конструктор нельзя, поэтому используется функция-фабрика, определяемая на стороне реализации. Эта функция обычно создает объект с помощью оператора new и возвращает указатель на созданный объект, приведенный к указателю на интерфейсный класс. Функция-фабрика может быть статическим членом интерфейсного класса, но это не обязательно, она, например, может быть членом специального класса-фабрики (который, в свою очередь, сам может быть интерфейсным) или свободной функцией. Функция-фабрика может возвращать не сырой указатель на интерфейсный класс, а интеллектуальный. Этот вариант рассмотрен в разделах 3.3.4 и 4.3.2.

3.3. Удаление объекта

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

Существуют четыре основных варианта:

3.3.1. Использование оператора delete

3.3.2. Использование специальной виртуальной функции

В этом варианте попытка удаления объекта с помощью оператора delete может компилироваться и даже выполняться, но это является ошибкой. Для ее предотвращения в интерфейсном классе достаточно иметь пустой или чисто виртуальный защищенный деструктор (см. раздел 1.3). Отметим, что использование оператора delete может оказаться достаточно сильно замаскированным, например, стандартные интеллектуальные указатели для удаления объекта по умолчанию используют оператор delete и соответствующий код глубоко «зарыт» в их реализации. Защищенный деструктор позволяет обнаружить все такие попытки на этапе компиляции.

3.3.3. Использование внешней функции

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

3.3.4. Автоматическое удаление с помощью интеллектуального указателя

3.4. Другие варианты управления временем жизни экземпляра класса реализации

3.5. Семантика копирования

Использование оператора копирующего присваивания не запрещено, но нельзя признать удачной идеей. Оператор копирующего присваивания всегда является парным, он должен идти в паре с копирующим конструктором. Оператор, генерируемый компилятором по умолчанию, бессмыслен, он ничего не делает. Теоретически можно объявить оператор присваивания чисто виртуальным с последующим переопределением, но виртуальное присваивание является не рекомендуемой практикой, подробности можно найти в [Meyers1]. К тому же присваивание выглядит весьма неестественно: доступ к объектам класса реализации обычно осуществляется через указатель на интерфейсный класс, поэтому присваивание будет выглядеть так:

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

Запретить присваивание можно двумя способами.

3.6. Конструктор интерфейсного класса

Часто конструктор интерфейсного класса не объявляется. В этом случае компилятор генерирует конструктор по умолчанию, необходимый для реализации наследования (см. раздел 1.1). Этот конструктор открытый, хотя достаточно, чтобы он был защищенным. Если в интерфейсном классе копирующий конструктор объявлен удаленным ( =delete ), то генерация компилятором конструктора по умолчанию подавляется, и необходимо явно объявить такой конструктор. Естественно его сделать защищенным с определением по умолчанию ( =default ). В принципе, объявление такого защищенного конструктора можно делать всегда. Пример находится в разделе 4.4.

3.7. Двунаправленное взаимодействие

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

3.8. Интеллектуальные указатели

Если интерфейсный класс поддерживает счетчик ссылок, то целесообразно использовать не стандартные интеллектуальные указатели, а специально написанный для такого случая, это сделать достаточно легко.

3.9. Константные функции-члены

Следует с осторожностью объявлять функции-члены интерфейсных классов как const. Одним из важных достоинств интерфейсных классов является возможность максимально полного отделения интерфейса от реализации, но ограничения, связанные с константностью функции-члена, могут создать проблемы при разработке класса реализации.

Читайте также:  прибалтика это какие страны и столицы

3.10. COM-интерфейсы

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

3.11. Интерфейсные классы и библиотеки

Достаточно часто интерфейсные классы используются в качестве интерфейса (API) для целых библиотек (SDK). В этом случае целесообразно следовать следующей схеме. Библиотека имеет доступную функцию-фабрику, которая возвращает указатель на интерфейсный класс-фабрику, с помощью которого и создаются экземпляры классов реализации других интерфейсных классов. В этом случае для библиотек, поддерживающих явную спецификацию экспорта (Windows DLL), требуется всего одна точка экспорта: вышеупомянутая функция-фабрика. Весь остальной интерфейс библиотеки становится доступным через таблицы виртуальных функций. Именно такая схема позволяет максимально просто реализовывать гибкие, динамические решения, когда модули подгружаются выборочно во время исполнения. Модуль загружается с помощью LoadLibrary() или ее аналогом на других платформах, далее получается адрес функции-фабрики, и после этого библиотека становится полностью доступной.

4. Пример интерфейсного класса и его реализации

4.1. Интерфейсный класс

Так как интерфейсный класс редко бывает один, то обычно целесообразно создать базовый класс.

Вот демонстрационный интерфейсный класс.

4.2. Класс реализации

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

4.3. Стандартные интеллектуальные указатели

4.3.1. Создание на стороне клиента

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

Для std::unique_ptr<> класс-удалитель является шаблонным параметром:

Отметим, что благодаря тому, что класс-удалитель не содержит данных, размер UniquePtr равен размеру сырого указателя.

Вот шаблон функции-фабрики:

Вот шаблон преобразования из сырого указателя в интеллектуальный:

А этот ошибочный код благодаря защищенному деструктору не компилируется (конструктор должен принимать второй аргумент — объект-удалитель):

Описанная схема имеет недостаток: через интеллектуальный указатель можно вызвать виртуальную функцию удаления объекта реализации, что приведет к двойному удалению. Эту проблему можно решить так: сделать виртуальную функцию удаления защищенной, а класс-удалитель другом. Пример находится в разделе 4.4.

4.3.2. Создание на стороне реализации

Интеллектуальный указатель можно создавать на стороне реализации. В этом случае клиент получает его в качестве возвращаемого значения функциии-фабрики. Если использовать std::shared_ptr<> и в его конструктор передать указатель на класс реализации, который имеет открытый деструктор, то пользовательский удалитель не нужен (и не требуется специальная виртуальная функция для удаления объекта реализации). В этом случае конструктор std::shared_ptr<> (а это шаблон) создает объект-удалитель по умолчанию, который базируется на типе аргумента и при удалении применяет оператор delete к указателю на объект реализации. Для std::shared_ptr<> объект-удалитель входит в состав экземпляра интеллектуального указателя (точнее его управляющего блока) и тип объекта-удалителя не влияет на тип интеллектуального указателя. В этом варианте предыдущий пример можно переписать так.

Для функции-фабрики более оптимальным является вариант с использованием шаблона std::make_shared<>() :

4.4. Альтернативная реализация базового класса

Чисто виртуальный деструктор нужно определить, Delete() не чисто виртуальная функция, поэтому ее также нужно определить.

5. Исключения и коллекции, реализованные с помощью интерфейсных классов

5.1 Исключения

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

Реализовать Exception можно, например, следующим образом.

Класс реализации IException :

Определение конструктора Exception :

5.2 Коллекции

Шаблон интерфейсного класса-коллекции может выглядеть следующим образом:

С такой коллекцией уже можно достаточно комфортно работать, но при желании указатель на такой шаблонный класс можно обернуть в шаблон класса-контейнера, который предоставляет интерфейс в стиле контейнеров стандартной библиотеки.

6. Интерфейсные классы и классы-обертки

7. Итоги

Объект реализации интерфейсного класса создается функцией-фабрикой, которая возвращает указатель или интеллектуальный указатель на интерфейсный класс.

Для удаления объекта реализации интерфейсного класса существуют три варианта.

В первом варианте интерфейсный класс должен иметь открытый виртуальный деструктор.

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

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

Список литературы

[GoF]
Гамма Э., Хелм Р., Джонсон Р., Влиссидес Дж. Приемы объектно-ориентированного проектирования. Паттерны проектирования.: Пер. с англ. — СПб.: Питер, 2001.

[Josuttis]
Джосаттис, Николаи М. Стандартная библиотека C++: справочное руководство, 2-е изд.: Пер. с англ. — М.: ООО «И.Д. Вильямс», 2014.

[Dewhurst]
Дьюхэрст, Стефан К. Скользкие места C++. Как избежать проблем при проектировании и компиляции ваших программ.: Пер. с англ. — М.: ДМК Пресс, 2012.

[Meyers1]
Мейерс, Скотт. Наиболее эффективное использование C++. 35 новых рекомендаций по улучшению ваших программ и проектов.: Пер. с англ. — М.: ДМК Пресс, 2000.

[Meyers2]
Мейерс, Скотт. Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ.: Пер. с англ. — М.: ДМК Пресс, 2014.

[Meyers3]
Мейерс, Скотт. Эффективный и современный C++: 42 рекомендации по использованию C++11 и C++14.: Пер. с англ. — М.: ООО «И.Д. Вильямс», 2016.

[Sutter]
Саттер, Герб. Решение сложных задач на C++.: Пер. с англ. — М: ООО «И.Д. Вильямс», 2015.

[Sutter/Alexandrescu]
Саттер, Герб. Александреску, Андрей. Стандарты программирования на С++.: Пер. с англ. — М.: ООО «И.Д. Вильямс», 2015.

Источник

Сказочный портал