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

  • Tutorial
Когда-то я собеседовался на должность 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

UPDATE:
  •  CV
  •  LTO
  •  PCH
  •  PGO
  •  SEH/VEH
  •  TMP
  •  VLA

Базовые вещи


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):

  • не имеет виртуальных методов
  • не имеет виртуальных базовых типов
  • не имеет нестатических полей-ссылок
  • все нестатические поля имеют одинаковый модификатор доступа (public/protected/private)
  • все нестатические поля и базовые классы — тоже типы со стандартным устройством
  • все нестатические поля и самого класса и всех его предков объявлены в каком-то одном классе (т. е. в самом классе или в одном из предков)
  • не наследует одному типу дважды, т. е. так нельзя:
    struct A {};
    struct B : A {};
    struct C : A{};
    struct D : B, C {};
  • тип первого нестатического поля или, если это массив, тип его элемента не должен совпадать ни с одним из базовых типов (из-за обязательной в данном случае EBO)

Впрочем, в 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.hpp"

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).
  • Время компиляции: т. к. публичный заголовок ничего не знает о реализации, он не подключает множество нужных ей заголовков. Соответственно уменьшается количество неявно подключаемых заголовков в клиентском коде. Еще упрощается поиск имен и разрешение перегрузок, т. к. публичный заголовок не содержит закрытых членов (они хоть и закрытые, но участвуют в этих процессах).

Цена, т. е. недостатки:

  • Плюс как минимум одно разыменование указателя и плюс один вызов функции при обращении к публичных методам.
  • Размер необходимой классу памяти увеличивается на размер указателя.
  • Часть этой памяти (скорее всего большая) выделяется в куче, что так же отрицательно сказывается на производительности.
  • Легко может нарушаться логическая константность. Например такой код скомпилируется:

    void Foo::doThis() const
    {
        pImpl_->doThis();     // cosnt method
        pImpl_->doSmthElse(); // non-const method
    }

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

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

RAII


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

Как ни странно (в первую очередь для программистов на C++), эта идиома используется и в других языках, даже в тех, где существует сборщик мусора. В Java это try-с-ресурсами, в Python – оператор with, в C# – директива using, в Go – defer. Но именно в 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 не дается бесплатно, пусть немного, но он отрицательно влияет на производительность и размер потребляемой памяти (отсюда частый совет не использовать dynamic_cast из-за его медлительности). Поэтому компиляторы, как правило, позволяют отключить RTTI. GCC и MSVC обещают, что на корректности перехвата исключений это не скажется.

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

STL


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

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

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

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

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.

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

Иногда 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 = bar();
}

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

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

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, стала обязательной, и соответствующие конструктор и оператор присваивания не нужны.

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

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

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

UDPATE


Спасибо PyerK за этот дополнительный список аббревиатур.

CV


Квалификаторы типа const и volatile. const означает, что объект/переменную нельзя модифицировать, попытка сделать это приведет либо в ошибке во время компиляции, либо к UB во время выполнения. volatile означает, что объект/переменная может измениться независимо от действий программы (например, какой-нибудь микроконтроллер заполняет пишет что-то в память), и компилятор не должен оптимизировать доступ к ней. Доступ к volatile объекту не через volatile ссылку или указатель так же приводит к UB.

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

LTO


Link Time Optimization. Оптимизация линковки. Как очевидно из названия, эта оптимизация происходит во время линковки, т. е. после компиляции. Линковщик может сделать то, на что не решился компилятор: сделать некоторые функции встраиваемыми, выкинуть неиспользуемые код и данные. Увеличивает время линковки, естественно.

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

PCH


Precompiled Headers. Предварительно откомпилированные заголовки. Часто используемые, но редко изменяемые заголовочные файлы компилируются один раз и сохраняются во внутреннем формате компилятора. Таким образом на повторную сборку проекта уйдет меньше времени, иногда значительно меньше.

Почитать еще: раз (рус.)

PGO


Profile-Guided Optimization. Оптимизация по результатам профайлинга. Это метод оптимизации программы, но не через статический анализ кода, а через тестовые запуски программы и сбор реальной статистики. Например, таким образом могут быть оптимизированы ветвления и вызовы виртуальных фукнций.

Почитать еще: раз (рус.)

SEH/VEH


Structured/Vectored Exception Handling. Это расширение MSVC для обработки исключений и ошибок. В отличие от стандартного try-catch SEH использует свои собственные ключевые слова: __try, __except, __finally, ловит и обрабатывает не явно выброшенные исключения, а такие ситуации, как доступ к невалидной памяти, переполнение стека из-за бесконечной рекурсии, вызов чисто виртуальной функции и т. п. VEH не ловит каждую ошибку явно, а создает глобальную цепочку обработчиков ошибок.

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

TMP


Template Meta-Programming. Шаблонное метапрограммирование. Метапрограммирование — это когда одна программа создает другую как результат своей работы. Шаблоны в C++ реализуют такое метапрограммирование. Компилятор по шаблону генерирует нужное количество классов или функций. Известно, что TMP в C++ является полным по Тьюрингу, т. е. на нем можно реализовать любую функцию.

Почитать еще: раз (рус.)

VLA


Variable-Length Arrays. Массивы переменной длины. Т.е. массивы, длина которых неизвестна на этапе компиляции:
void foo(int n)
{
    int array[n];
}

Стандарт C++ такое не разрешает. Что несколько странно, т. к. они существуют в чистом C еще со стандарта C99. И поддерживаются некоторыми компиляторами C++ в виде расширения.

Почитать еще: раз (рус.)

P. S.


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

Вторая часть
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 27

    0
    В описании вакансий часто можно встретить 2 отдельных требования — знание C++ и знакомство с STL.

    Смею предположить, что когда STL фигурирует в вакансии, ожидается не только знакомство, но и знание внутреннего устройства базовых реализаций контейнеров/алгоритмов и скорости работы. Однажды проходил собеседование без должной подготовки и не смог рассказать ничего внятного про std::map (спойлер: не прошёл).


    За подборку большое спасибо.

      0
      По моей статистике, вопросы про назначение и преимущества\недостатки контейнеров std::map и std::deque ставят в затруднительное положение даже разработчиков с многолетним стажем в C++.
        0

        Еще есть вероятность, что человек занимается разработкой под встраиваемые системы и там часто no std

        0
        Про SBO возможно стоит упомянуть трюк из С, но для этого придётся переставить порядок элементов — последним должен идти массив символов (по-моему flexible array member можно даже нулевого размера). Можно выделить через alloca нужный размер (больше чем сам struct), после чего обращаться к символам вне диапазона задекларированного массива.
        Удобно при работе c POD
          +1
          TMP — Шаблонное метапрограммирование
          CV — const volatile (qualifiers)
          ABI — упомянута но не раскрыта
          LTO — оптимизация линковки
          PCH — pre compiled header
          PGO — оптимизация по результатам профайлинга
          SEH\VEH — обработчики исключений (win)
          VLA — фича из C но употрбляется и в С++ контексте
            0
            Про ABI будет во второй части. Остальное добавлю кратким списком, спасибо.
              0
              В тему UB вспомнилась аббревиатура NDR no diagnostic required.
            –2
            После C++ переходить на Java это такое облегчение)
              +5
              Не хотелось бы начинать бесполезный спор, но не соглашусь :) Отсутствие в Java таких банальных вещей, как беззнаковые целые и значения аргументов по умолчанию обескураживает.
                +1

                Значение аргументов по умолчанию — зло :) Поубивал бы людей, которые их придумали) Столько дебильных багов из-за них словлено и отыскать их в коде оч трудно глазами.


                Особенно они хороши в таких замечательных вещах, как виртуальные функции :) Например что выведет этот код:


                struct A {
                    virtual void foo(int a = 5) 
                    {
                        std::cout << a << std::endl;
                    } 
                };
                
                struct B : A
                {
                      void foo(int a = 7) override
                      {
                         std::cout << a*2 << std::endl;
                      }
                };
                
                A a;
                B b;
                
                auto pa = dynamic_cast<A*>(&b);
                
                pa->foo();

                В целом, никакого глубокого познания в С++ не требуется, чтобы понять что этот код должен вывести, но в первый раз я впал в ступор потому что даже после пяти лет на плюсах я был НЕ УВЕРЕН в ответе, хоть и дал его правильный, когда спрашивали :)

                  0
                  А об этом, кстати, отдельно написано в книжке Майеса, довольно старой уже :)
                  В общем, подводных камней везде хватает, просто к C++ я уже привык.
                    0

                    Были б безболезненные типы может и багов было чуточку меньше.

                      0
                      Если кому-то интересно почему: значения аргументов по-умолчанию связываются статически (при компиляции). Поэтому они определяются статическим типом указателя. В данном случае pa имеет тип A*. Соответственно вызывается виртуальная B::foo() с аргументом a = 5.
                        0

                        Ну, тогда стоит отметить, что помимо аргументов по умолчанию, статически привязана и сигнатура функции. Несмотря на то, что вызывается B::foo(), мы об этом узнаем только в рантайме, поэтому не будет работать следующий код:


                        struct A {
                            // Нет аргумента по умолчанию!
                            virtual void foo(int a) {  
                                std::cout << a << std::endl;
                            } 
                        };
                        
                        struct B : A {
                              void foo(int a = 7) override {
                                 std::cout << a*2 << std::endl;
                              }
                        };
                        
                        A a;
                        B b;
                        
                        auto pa = dynamic_cast<A*>(&b);
                        
                        pa->foo(); //Ошибка в сигнатуре A::foo(), нет default аргумента.
                      0
                      Для меня основное разочарование в Java/c# это отсутствие приватного наследования, невозможность иметь невиртуальные функции/конструторы в интерфейсе (иными словами отличие интерфейса от класса). Отсутствие нормальной const логики (именно как в плюсах).
                    0

                    В недостатках CRTP


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

                    А что мешает писать так?


                    class Derived : Base<Derived>
                    {
                    private:
                      friend Base<Derived>;
                      void actionImpl() const { ... }
                    };
                      0
                      Ничего не мешает, но это все равно усложняет код и взаимоотношения классов.
                      0
                      Раз уж зашёл разговор о вопросах на собеседованиях. В Нидерландах как обстоят дела на собеседовании с такими вот вопросами по аббревиатурам и тому же STL? Есть отличия в том, что понимают под STL когда пишут у нас и у них?
                        0
                        Из того, что я сам испытал в Нидерландах, могу сказать, что на технических скиллах тут не зацикливаются. Если решил задачаки на Hackerrank/Leetcode/где-то еще — молодец, давай обсудим твой опыт.
                          0
                          Сколько их там нарешать нужно?
                            0
                            Немного, штуки 3-5.
                        0
                        Тривиальные вещи тоже можно не пропускать – это было бы полезно и начинающим прогерам, и любопытствующим из совсем других стеков технологий вроде меня (:
                          0
                          По моему у автора ошибка в RVO и NRVO примере.
                            0
                            Где именно?
                              0
                              ...
                              int main()
                              {
                                  auto f = bar();
                              }

                              должно быть
                                0
                                Слона-то я и не заметил. Спасибо.
                            0
                            Только я заметил, что в статье нет описания аббревиатур из вводной части «DRY, KISS, YAGNI, NIH»? Вам так и не удалось найти их описания? :)

                            Ой! Оказывается они во второй части описаны.

                            Only users with full accounts can post comments. Log in, please.