Хабрахабр

Шпаргалка по аббревиатурам C++ и не только. Часть 1: C++

Когда-то я собеседовался на должность C++ разработчика в одну приличную и даже известную контору. Опыт у меня тогда уже кое-какой был, я даже назывался ведущим разработчиком у тогдашнего своего работодателя. Но на вопросы о том, знаком ли я такими вещами, как DRY, KISS, YAGNI, NIH, раз за разом мне приходилось отвечать «Нет».

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

Это не значит, что они относятся только к C++, или что это все-все-все понятия из C++ (об идиомах языка можно тома писать).
Тогда-то я и решил составить (в первую очередь для себя) шпаргалку по аббревиатурам, которые полезно знать C++ разработчику. Ну и я пропустил совсем уж тривиальные вещи вроде LIFO, FIFO, CRUD, OOP, GCC и MSVC. Нет, это только реально встречавшиеся мне в работе и на собеседованиях понятия, обычно выражаемые в виде аббревиатур.

Когда это было уместно, я группировал понятия вместе, иначе — просто перечислял по алфавиту. Тем не менее аббревиатур набралось порядочно, поэтому шпаргалку я разделил на 2 части: сильно характерные для C++ и более общеупотребительные. В общем, большого смысла в их порядке нет.

Базовые вещи:
  •  ODR
  •  POD
  •  POF
  •  PIMPL
  •  RAII
  •  RTTI
  •  STL
  •  UB

Тонкости языка:
  •  ADL
  •  CRTP
  •  CTAD
  •  EBO
  •  IIFE
  •  NVI
  •  RVO и NRVO
  •  SFINAE
  •  SBO, SOO, SSO

Базовые вещи

ODR

One Definition Rule. Правило одного определения. Упрощенно означает следующее:

  • В пределах одной единицы трансляции каждая переменная, функция, класс и т. п. может иметь не более одного определения. Объявлений — сколько угодно (кроме перечислений без заданного базового типа, которые просто нельзя объявить, не определив), но определений — не больше одного. Можно меньше, если сущность не используется.
  • В рамках всей программы каждая используемая не-inline функция и переменная обязана иметь строго одно определение. Каждая используемая inline функция и переменная должна иметь одно определение в каждой единице трансляции.
  • Некоторые сущности — например классы, inline функции и переменный, шаблоны, перечисления и т. д. — могут иметь несколько определений в программе (но не больше одного в единице трансляции). Собственно это и происходит, когда в несколько .cpp файлов подключается один и тот же заголовок, содержащий полностью реализованный класс, например. Но эти определения должны совпадать (я сильно упрощаю, но суть такова). Иначе будет UB.

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

по Стандарту тут UB) и что-то может пропустить. Гораздо больше нарушений может найти линковщик, но, строго говоря, он не обязан этого делать (т. к. К тому же процесс поиска нарушений ODR на этапе линковки имеет квадратичную сложность, а сборка C++ кода и так не быстрая.

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

Почитать еще: раз (англ.), два (англ.)

POD

Plain Old Data. Простая структура данных. Самое простое определение: это такая структура, которую можно как есть, в бинарном виде отправить в/получить из C библиотеки. Или, что то же самое, правильно скопировать простым memcpy.

В новейшем на текущий момент C++17 POD определяется, как От Стандарта к Стандарту полное определение менялось в деталях.

  • скалярный тип
  • или класс/структура/объединение, который:
    — есть тривиальный класс
    — есть класс со стандартным устройством
    — не содержит не-POD не-статических полей
  • или массив таких типов

Тривиальный класс (trivial class):

  • имеет хотя бы по одному не удаленному:
    — конструктор по умолчанию
    — копирующий конструктор
    — перемещающий конструктор
    — копирующий оператор присваивания
    — перемещающий оператор присваивания
  • все конструкторы по умолчанию, копирующие и перемещающие конструкторы и операторы присваивания являются тривиальным (упрощенно — сгенерированными компилятором) или удаленными
  • имеет тривиальный не удаленный деструктор
  • все базовые типы и все поля классовых типов имеют тривиальные деструкторы
  • не имеет виртуальных методов (включая деструктор)
  • не имеет виртуальных базовых типов

Класс со стандартным устройством (standard layout class):
В прочем, в C++20 понятия POD типа уже не будет, останутся только тривиальный тип и тип со стандартным устройством.

Почитать еще: раз (рус.), два (англ.), три (англ.)

POF

Plain Old Function. Простая функция в стиле C. Упоминается в Стандарте до C++14 включительно только в контексте обработчиков сигналов. Требования к ней такие:

  • использует только общие для C и C++ вещи (т. е. никаких исключений и try-catch, например)
  • не вызывает косвенно или непосредственно не-POF фукнции, за исключение атомарных, свободных от блокировок операций (std::atomic_init, std::atomic_fetch_add и т. п.)

Только такие функции, имеющие к тому же C линковку (extern "C"), разрешается Стандартом использовать в качестве обработчиков сигналов. Поддержка других функций зависит от компилятора.

В таких вычислениях запрещены: В C++17 понятие POF исчезает, вместо него появляется безопасное в смысле сигналов вычисление (signal-safe evaluation).

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

Если обработчик сигнала делает что-то из вышеперечисленного, Стандарт обещает UB.

Почитать еще: раз (англ.)

PIMPL

Pointer To Implementation. Указатель на реализацию. Классическая идиома в C++, так же известная как d-pointer, opaque pointer, compilation firewall. Заключается в том, что все закрытые методы, поля и прочие детали реализации некоего класса выделяются в отдельный класс, а в исходном классе остаются только публичные методы (т. е. интерфейс) и указатель на экземпляр этого нового отдельного класса. Например:

foo.hpp

class Foo
{
public: Foo(); ~Foo(); void doThis(); int doThat(); private: class Impl; std::unique_ptr<Impl> pImpl_;
};

foo.cpp

#include "foo.h" class Foo::Impl
{
// implementation
}; Foo::Foo() : pImpl_(std::make_unique<Impl>())
Foo::~Foo() = default; void Foo::doThis()
{ pImpl_->doThis();
} int Foo::doThat()
{ return pImpl_->doThat();
}

Зачем это надо, т. е. преимущества:

  • Инкапсуляция: пользователи класса через подключение заголовка получают только то, что им надо — публичный интерфейс. Если детали реализации изменятся, код клиента не придется перекомпилировать (см. ABI во второй части).
  • Время компиляции: т. к. публичный заголовок ничего не знает о реализации, он не подключает множество нужных ей заголовков. Соответственно уменьшается количество неявно подключаемых заголовков в клиентском коде. Еще упрощается поиск имен и разрешение перегрузок, т. к. публичный заголовок не содержит закрытых членов (они хоть и закрытые, но участвуют в этих процессах).

Цена, т. е. недостатки:
Некоторые из этих недостатков устранимы, но цена — дальнейшее усложнение кода и введение дополнительных уровней абстракции (см. FTSE во второй части).

Почитать еще: раз (рус.), два (рус.), три (англ.)

RAII

Resource Acquisition Is Initialization. Захват ресурса есть инициализация. Смысл этой идиомы в том, что удержание некоторого ресурса длится в течении жизни соответствующего объекта. Захват ресурса происходит в момент создания/инициализации объекта, освобождение — в момент разрушения/финализации этого же объекта.

В Java это try-с-ресурсами, в Python – оператор with, в C# – директива using, в Go – defer. Как ни странно (в первую очередь для программистов на C++), эта идиома используется и в других языках, даже в тех, где существует сборщик мусора. Но именно в C++ с его абсолютно предсказуемой жизнью объектов RAII вписывается особенно органично.

Например, умные указатели так управляют памятью, файловые потоки — файлами, локи мьютексов — мьютексами. В C++ обычно ресурс захватывается в конструкторе и освобождается в деструкторе. Т.е. Прелесть в том, что не зависимо от того, как происходит выход из блока (scope) – нормально ли через любую из точек выхода, или было брошено исключение — управляющий ресурсом объект, созданный в этом блоке, будет уничтожен, а ресурс — освобожден. помимо инкапсуляции RAII в C++ еще и помогает обеспечивать безопасность в смысле исключений.

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

Почитать еще: раз (рус.), два (англ.)

RTTI

Run-Time Type Information. Идентификация типа во время исполнения. Это механизм, позволяющий получить информацию о типе объекта или выражения во время выполнения. Существует и в других языках, а в C++ он используется для:

  • dynamic_cast
  • typeid и type_info
  • перехвата исключений

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

Поэтому компиляторы, как правило, позволяют отключить RTTI. RTTI не дается бесплатно, пусть немного, но он отрицательно влияет на производительность и размер потребляемой памяти (отсюда частый совет не использовать dynamic_cast из-за его медлительности). GCC и MSVC обещают, что на корректности перехвата исключений это не скажется.

Почитать еще: раз (рус.), два (англ.)

STL

Standard Template Library. Стандартная библиотека шаблонов. Часть стандартной библиотеки C++, предоставляющая обобщенные контейнеры, итераторы, алгоритмы и вспомогательные функции.

Из разделов Стандарта к STL однозначно можно отнести Containers library, Iterators library, Algorithm library и частично General utilities library. Не смотря на известное имя, STL никогда так не называлась в Стандарте.

Я никогда этого не понимал, ведь STL — неотъемлемая часть языка с первого Стандарта 1998 года. В описании вакансий часто можно встретить 2 отдельных требования — знание C++ и знакомство с STL.

Почитать еще: раз (рус.), два (англ.)

UB

Undefined Behavior. Неопределенное поведение. Это поведение в тех ошибочных случаях, для которых Стандарт не имеет никаких требований. Многие из них явно перечислены в Стандарте как приводящие к UB. К ним, например, относятся:

  • нарушение границ массива или STL контейнера
  • использование неинициализированной переменной
  • разыменование нулевого указателя
  • переполнение целых со знаком

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

Неуточняемое поведение — это корректное поведение корректной программы, но которое с разрешения Стандарта зависит от компилятора. С другой стороны, неопределенное поведение не стоит путать с неуточняемым поведением (unspecified behavior). Например, это порядок вычисления аргументов функции или детали реализации std::map. И компилятор не обязан документировать его.

От неуточняемого отличается наличием документации. Ну и тут же можно вспомнить про поведение, зависящее от реализации (implementation-defined behavior). Пример: размер std::size_t.

Почитать еще: раз (рус.), два (рус.), три (англ.)

Тонкости языка

ADL

Argument-Dependent Lookup. Поиск, зависящий от аргументов. Он же поиск Кёнига — в честь Andrew Koenig. Это набор правил для разрешения неквалифицированных имен функций (т. е. имен без оператора ::), дополнительный к обычному разрешению имен. Упрощенно: имя функции ищется в пространствах имен, относящихся к ее аргументам (это пространство, содержащее тип аргумента, сам тип, если это класс, все его предки и т.п.).

Простейший пример

#include <iostream> namespace N
{ struct S {}; void f(S) { std::cout << "f(S)" << std::endl; };
} int main()
{ N::S s; f(s);
}

Функция f найдена в пространстве имен N только потому, что ее аргумент принадлежит этому пространству.

Даже банальный std::cout << "Hello World!\n" использует ADL, т. к. std::basic_stream::operator<< не перегружен для const char*. Но первым аргументом этого оператора является std::basic_stream, и компилятор ищет и находит подходящую перегрузку в пространстве имен std.

Или есть имя функции указано в скобках (пример выше не скомпилируется с (f)(s); придется писать (N::f)(s);). Некоторые детали: ADL не применяется, если обычный поиск нашел объявление члена класса, или объявление функции в текущем блоке без использования using, или объявление не функции и не шаблона функции.

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

Например, этот код не скомпилируется

namespace N1
{ struct S {}; void foo(S) {};
} namespace N2
{ void foo(N1::S) {}; void bar(N1::S s) { foo(s); }
}

Почитать еще: раз (англ.), два (англ.), три (англ.)

CRTP

Curiously Recurring Template Pattern. Странно рекурсивный шаблон. Суть шаблона в следующем:

  • некий класс наследуется от шаблонного класса
  • класс-наследник используется как параметр шаблона своего базового класса

Проще привести пример:

template <class T>
struct Base {}; struct Derived : Base<Derived> {};

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

Пример

template <typename T>
struct Base
{ void action() const { static_cast<T*>(this)->actionImpl(); }
}; struct Derived : Base<Derived>
{ void actionImpl() const { ... }
}; template <class Arg>
void staticPolymorphicHandler(const Arg& arg)
{ arg.action();
}

При правильном использовании T всегда является потомком Base, поэтому для приведения достаточно static_cast. Да, в данном случае базовый класс знает интерфейс потомка.

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

  • struct Derived : singleton<Derived> { … }
  • struct Derived : private boost::noncopyable<Derived> { … }
  • struct Derived : std::enable_shared_from_this<Derived> { … }
  • struct Derived : counter<Derived> { … } — подсчет числа созданных и/или существующих объектов

Недостатки, или, скорее, требующие внимания моменты:

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

    Пример

    template <typename T>
    struct Base {}; struct Derived1 : Base<Derived1> {};
    struct Derived2 : Base<Derived1> {};

    Но можно добавить защиту:

    private: Base() = default; friend T;

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

Почитать еще: раз (рус.), два (англ.)

CTAD

Class Template Argument Deduction. Автоматический вывод типа параметра шаблона класса. Это новая возможность из C++17. Раньше автоматически выводились только типы переменных (auto) и параметры шаблонов функций, из-за чего и возникли вспомогательные функции типа std::make_pair, std::make_tuple и т. п. Теперь они по большей части не нужны, т. к. компилятор способен автоматически вывести и параметры шаблонов классов:

std::pair p{1, 2.0}; // -> std::pair<int, double>
auto lck = std::lock_guard{mtx}; // -> std::lock_guard<std::mutex>

CTAD – новая возможность, ей еще развиваться и развиваться (С++20 уже обещает улучшения). Пока же ограничения таковы:

  • Не поддерживается частичный вывод типов параметров

    std::pair<double> p{1, 2}; // ошибка
    std::tuple<> t{1, 2, 3}; // ошибка

  • Не поддерживаются псевдонимы шаблонов

    template <class T, class U>
    using MyPair = std::pair<T, U>;
    MyPair p{1, 2}; // ошибка

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

    template <class T>
    struct Wrapper {}; template <>
    struct Wrapper<int>
    { Wrapper(int) {};
    };
    Wrapper w{5}; // ошибка

  • Не поддерживаются вложенные шаблоны

    template <class T>
    struct Foo
    { template <class U> struct Bar { Bar(T, U) {}; };
    };
    Foo::Bar x{ 1, 2.0 }; // ошибка
    Foo<int>::Bar x{1, 2.0}; // OK

  • Очевидно, CTAD не сработает, если тип параметра шаблона никак не связан с аргументами конструктора

    template <class T>
    struct Collection
    { Collection(std::size_t size) {};
    };
    Collection c{5}; // ошибка

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

Пример

template <class T>
struct Collection
{ template <class It> Collection(It from, It to) {};
};
Collection c{v.begin(), v.end()}; // ошибка template <class It>
Collection(It, It)->Collection<typename std::iterator_traits<It>::value_type>;
Collection c{v.begin(), v.end()}; // теперь OK

Почитать еще: раз (рус.), два (англ.)

EBO

Empty Base Optimization. Оптимизация пустого базового класса. Так же может называться Empty Base Class Optimization (EBCO).

Иначе сломается вся арифметика указателей, т. к. Как известно, в C++ размер объекта любого класса не может быть нулем. Поэтому даже объекты пустых классов (т. е. по одному адресу будет возможно разметить сколько угодно разных объектов. классов без единого нестатического поля) имеют какой-то ненулевой размер, который зависит от компилятора и ОС и обычно равен 1.

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

Пример

struct Empty {}; struct Foo : Empty
{ int i;
}; std::cout << sizeof(Empty) << std::endl; // 1
std::cout << sizeof(Foo) << std::endl; // 4
std::cout << sizeof(int) << std::endl; // 4

Но т. к. по одному адресу все-таки не могут размещаться разные объекты одного типа, EBO не сработает, если:

  • Пустой класс дважды встречается среди предков

    struct Empty {}; struct Empty2 : Empty {}; struct Foo : Empty, Empty2
    { int i;
    }; std::cout << sizeof(Empty) << std::endl; // 1
    std::cout << sizeof(Empty2) << std::endl; // 1
    std::cout << sizeof(Foo) << std::endl; // 8

  • Первое нестатическое поле является объектом того же пустого класса или его наследника

    struct Empty {}; struct Foo : Empty
    { Empty e; int i;
    }; std::cout << sizeof(Empty) << std::endl; // 1
    std::cout << sizeof(Foo) << std::endl; // 8

В случаях же когда объекты пустых классов являются нестатическими полями, никаких оптимизаций не предусмотрено (это пока, в C++20 появится атрибут [[no_unique_address]]). Но тратить по 4 байта (или сколько компилятору надо) на каждое такое поле обидно, поэтому можно самостоятельно «схлопнуть» объекты пустых классов с первым непустым нестатическим полем.

Пример

struct Empty1 {};
struct Empty2 {}; template <class Member, class ... Empty>
struct EmptyOptimization : Empty ...
{ Member member;
}; struct Foo
{ EmptyOptimization<int, Empty1, Empty2> data;
};

Странно, но в этом случае размер Foo получается разным у разных компиляторов, у MSVC 2019 это 8, у GCC 8.3.0 это 4. Но в любом случае увеличение числа пустых классов на размер Foo не влияет.

Почитать еще: раз (англ.), два (англ.)

IIFE

Immediately-Invoked Function Expression. Немедленно вызываемое функциональное выражение. Вообще это идиома в JavaScript, откуда Джейсон Тёрнер (Jason Turner) ее и позаимствовал вместе с названием. По факту это просто создание и немедленный вызов лямбды:

const auto myVar = [&] { if (condition1()) { return computeSomeComplexStuff(); } return condition2() ? computeSonethingElse() : DEFAULT_VALUE;
} ();

Зачем это надо? Ну например, как в приведенном коде для того, чтобы инициализировать константу результатом нетривиального вычисления и не засорить при этом область видимости лишними переменными и функциями.

Почитать еще: раз (англ.), два (англ.)

NVI

Non-Virtual Interface. Невиртуальный интерфейс. Согласно этой идиоме открытый интерфейс класса не должен содержать виртуальных функций. Все виртуальные функции делаются закрытыми (максимум защищенными) и вызываются внутри открытых невиртуальных.

Пример

class Base
{
public: virtual ~Base() = default; void foo() { // check precondition fooImpl(); // check postconditions } private: virtual void fooImpl() = 0;
}; class Derived : public Base
{
private: void fooImpl() override { }
};

Зачем это надо:

  • Каждая открытая виртуальная функция делает 2 вещи: определяет открытый интерфейс класса и участвует в переопределении поведения в классах-потомках. Применение NVI избавляет от таких функций с двойной нагрузкой: интерфейс задается одними функциями, изменение поведения — другими. Можно менять и то, и другое независимо друг от друга.
  • Если для всех вариантов реализации виртуальной функции есть некие общие требования (пред- и пост-проверки, захват мьютекса и т. п.), то очень удобно собрать их в одном месте (см. DRY во второй части) — в базовом классе — и запретить наследникам переопределять это поведение. Т.е. получается частный случай паттерна Шаблонный метод.

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

Почитать еще: раз (англ.), два (англ.)

RVO и NRVO

(Named) Return Value Optimization. Оптимизация (именованного) возвращаемого значения. Это частный случай разрешенного Стандартом copy elision – компилятор может опустить ненужные копирования временных объектов, даже если их конструкторы и деструкторы имеют явные побочные эффекты. Такая оптимизация допустима, когда функция возвращает объект по значению (два других разрешенных случая copy elision – это выброс и поимка исключений).

Пример

Foo bar()
{ return Foo();
} int main()
{ auto f = Foo();
}

Без RVO здесь был бы создан временный объект Foo в функции bar, потом через конструктор копирования из него был бы создан еще один временный объект в функции main (чтобы получить результат bar), и только потом был бы создан объект f и ему было бы присвоено значение второго временного объекта. RVO избавляется от всех этих копирований и присваиваний, и функция bar создает непосредственно f.

Функция bar (работающая уже в своем фрейме), получает доступ к этой памяти, выделенной в предыдущем фрейме и создает там нужный объект. Происходит это примерно так: функция main выделяет в своем фрейме стека место под объект f.

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

Пример

Foo bar()
{ Foo result; return result;
}

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

Здесь NRVO не работает

Foo bar(bool condition)
{ if (condition) { Foo f1; return f1; } Foo f2; return f2;
}

Практически все компиляторы давно поддерживают RVO. Степень поддержки же NRVO может варьироваться от компилятора к компилятору и от версии к версии.

И хотя копирующие конструктор и оператор присваивания не вызываются, они должны быть у класса объекта. RVO и NRVO – это всего лишь оптимизации. Правила немного поменялись в C++17: теперь RVO не считается copy elision, стала обязательной, и соответствующие конструктор и оператор присваивания не нужны.

До C++14 включительно об этом ничего не сказано, C++17 требует RVO в таких выражениях, а грядущий C++20 – запрещает. Внимание: (N)RVO в константных выражениях — скользкая тема.

Во-первых, (N)RVO все-таки эффективнее, т.к. Пара слов о связи с семантикой перемещения. Во-вторых, если вместо result из той же функции возвращать std::move(result), то NRVO гарантированно не сработает. не надо вызывать конструктор перемещения и деструктор. Перефразируя Стандарт: RVO применяется к prvalue, NRVO – к lvalue, a std::move(result) – это xvalue.

Почитать еще: раз (англ.), два (англ.), три (англ.)

SFINAE

Substitution Failure Is Not An Error. Неудачная подстановка — не ошибка. SFINAE — это особенность процесса инстанциации шаблонов — функций и классов — в С++. Суть в том, что если некий шаблон не получается инстанциировать, это не считается ошибкой, если есть другие варианты. Например, упрощенно алгоритм выбора наиболее подходящей перегрузки функций работает так:

  1. Происходит разрешение имени функции — компилятор ищет все функции с данным именем во всех рассматриваемых пространствах имен (см. ADL).
  2. Отбрасываются неподходящие функции — не то количество аргументов, нет нужного преобразования типов аргументов, не удалось вывести типы для шаблона функции, и т. п.
  3. Из оставшихся кандидатов формируется набора так называемых жизнеспособных функций (viable functions), из которого компилятор должен выбрать строго одну наиболее подходящую функцию. Если набор получился пустой или не получилось выбрать одну функцию — мы получаем соответствующую ошибку компиляции.

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

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

Пример

#include <iostream>
#include <type_traits>
#include <utility> template <class, class = void>
struct HasToString : std::false_type
{}; // это частичная специализация шаблона, и потому при разрешении перегрузки // имеет приоритет - если типы получится вывести, конечно
// а если не получится — не беда, выше есть общий вариант, подходящий всем
template <class T>
struct HasToString<T, std::void_t<decltype(&T::toString)>> : std::is_same<std::string, decltype(std::declval<T>().toString())>
{}; struct Foo
{ std::string toString() { return {}; }
}; int main()
{ std::cout << HasToString<Foo>::value << std::endl; // 1 std::cout << HasToString<int>::value << std::endl; // 0
}

Появившийся в C++17 static if может в некоторых случаях заменить SFINAE, а ожидаемые в C++20 концепты чуть ли вообще не сделают ее ненужной. Посмотрим.

Почитать еще: раз (рус.), два (англ.), три (англ.)

SBO, SOO, SSO

Small Buffer/Object/String Optimization. Оптимизация малых буферов/объектов/строк. Иногда встречается SSO в значении Small Size Optimization, но очень редко, поэтому будем считать, что SSO – это про строки. SBO и SOO – просто синонимы, а SSO – наиболее известный частный случай.

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

Например, std::string можно было бы реализовать так:

Пример

class string
{ char* begin_; size_t size_; size_t capacity_;
};

Размер такого класса у меня получается 24 байта (зависит от компилятора и платформы). Т.е. строки не длиннее 24 символов можно было бы размещать на стеке. На самом не до 24, конечно, т. к. надо как-то различать размещение на стеке и в куче. Но вот простейший способ для коротких строк до 8 символов (размер тот же — 24 байта):

Пример

class string
{ union Buffer { char* begin_; char local_[8]; }; Buffer buffer_; size_t _size; size_t _capacity;
};

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

А вот std::vector никогда не оптимизируется таким образом, т. к. Почти все реализации std::string используют SSO и как минимум некоторые реализации std::function. SBO не позволит выполнить эти требования (для std::string их нет). Стандарт требует, чтобы std::swap для двух векторов не вызывала копирования или присваивания их элементов, и чтобы все валидные итераторы оставались валидными. Зато boost::container::small_vector, как легко догадаться, использует SBO.

Почитать еще: раз (англ.), два (англ.)

P. S.

Если я что-то упустил или где-то ошибся — пишите в комментариях. Только помните, пожалуйста, что здесь перечислены только аббревиатуры, непосредственно относящиеся к C++. Для прочих, но не менее полезных, будет отдельный пост.

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»