Является ли `Ensures` тем же самым, что и `assert`?
Нет. Это заглушка для языковой поддержки постусловий контрактов.
Приложение A: Библиотеки
В этом разделе перечислены рекомендуемые библиотеки, а также явно рекомендуется несколько из них.
??? Подходит ли для общего руководства? Думаю, нет ???
Приложение B: Модернизация кода
В идеале мы следуем всем правилам во всём коде. На практике нам приходится иметь дело с большим количеством устаревшего кода:
- прикладной код, написанный до того, как рекомендации были сформулированы или стали известны
- библиотеки, написанные в соответствии со старыми/иными стандартами
- код, написанный в условиях «нестандартных» ограничений
- код, до модернизации которого руки просто не дошли
Если у нас есть миллион строк нового кода, идея «просто изменить всё сразу» в типичном случае нереалистична. Поэтому нам нужен способ постепенной модернизации кодовой базы.
Обновление старого кода до современного стиля может быть непростой задачей. Нередко старый код одновременно и запутан (труден для понимания), и работает корректно (для текущего диапазона использования). Как правило, первоначальный автор недоступен, а тестовые случаи неполны. Запутанность кода резко увеличивает трудозатраты на любое изменение и риск внесения ошибок. Нередко запутанный старый код работает излишне медленно, поскольку рассчитан на устаревшие компиляторы и не может воспользоваться преимуществами современного аппаратного обеспечения. Во многих случаях для масштабного обновления потребуется автоматизированная инструментальная поддержка в стиле «modernizer».
Цель модернизации кода — упростить добавление новой функциональности, облегчить сопровождение, повысить производительность (пропускную способность или задержку) и лучше использовать возможности современного аппаратного обеспечения. «Красивый внешний вид» или «соответствие современному стилю» сами по себе не являются причиной для изменений. Каждое изменение несёт в себе риски, а наличие устаревшей кодовой базы влечёт затраты (в том числе упущенные возможности). Снижение затрат должно превышать связанные с этим риски.
Но как это сделать?
Не существует единого подхода к модернизации кода. Оптимальный способ зависит от самого кода, срочности обновлений, опыта разработчиков и доступных инструментов. Вот несколько (весьма общих) идей:
В большинстве случаев это также невозможно.
Это будет набор изменений по всей кодовой базе, но, весьма вероятно, они принесут огромную пользу. После этого код, скрытый за этими интерфейсами, можно постепенно модернизировать, не затрагивая остальное.
- Идеал — «просто обновить всё сразу». Это даёт наибольшие преимущества за наименьшее суммарное время.
- Можно конвертировать кодовую базу модуль за модулем, однако правила, затрагивающие интерфейсы (особенно ABI), например использование
span, не могут применяться в рамках одного модуля. - Можно конвертировать код «снизу вверх», начиная с правил, которые, по нашей оценке, дадут наибольшую выгоду и/или наименьшее количество проблем в данной кодовой базе.
- Можно начать с фокуса на интерфейсах — например, убедиться, что никакие ресурсы не утекают и ни один указатель не используется некорректно.
Какой бы путь вы ни выбрали, обратите внимание: наибольшие преимущества достигаются при наибольшем соответствии рекомендациям. Рекомендации — это не случайный набор несвязанных правил, из которых можно выбирать произвольно в расчёте на успех.
Мы очень хотели бы услышать об опыте и используемых инструментах. Модернизация может быть значительно быстрее, проще и безопаснее при поддержке инструментов анализа и даже инструментов трансформации кода.
Приложение C: Обсуждения
В этом разделе представлены дополнительные материалы по правилам и наборам правил. В частности, здесь приводятся дополнительные обоснования, более подробные примеры и обсуждения альтернатив.
Обсуждение: Определяйте и инициализируйте элементы данных в порядке их объявления
Элементы данных всегда инициализируются в порядке их объявления в определении класса, поэтому пишите их в том же порядке в списке инициализации конструктора. Написание их в другом порядке лишь вносит путаницу, поскольку выполняться они будут не в том порядке, который вы видите, — а это может затруднить обнаружение ошибок, зависящих от порядка.
class Employee {
string email, first, last;
public:
Employee(const char* firstName, const char* lastName);
// ...
};
Employee::Employee(const char* firstName, const char* lastName)
: first(firstName),
last(lastName),
// BAD: first and last not yet constructed
email(first + "." + last + "@acme.com")
{}
В этом примере email будет сконструирован до first и last, потому что он объявлен первым. Это значит, что его конструктор попытается использовать first и last слишком рано — не просто до того, как им будут присвоены нужные значения, но до того, как они будут сконструированы вообще.
Если определение класса и тело конструктора находятся в разных файлах, то влияние порядка объявления элементов данных на корректность конструктора будет ещё сложнее заметить.
Ссылки:
[\[Cline99\]](#Cline99) §22.03-11, [\[Dewhurst03\]](#Dewhurst03) §52-53, [\[Koenig97\]](#Koenig97) §4, [\[Lakos96\]](#Lakos96) §10.3.5, [\[Meyers97\]](#Meyers97) §13, [\[Murray93\]](#Murray93) §2.1.3, [\[Sutter00\]](#Sutter00) §47
Обсуждение: Использование =, {} и () в качестве инициализаторов
???
Обсуждение: Используйте фабричную функцию, если вам нужно «виртуальное поведение» при инициализации
Если ваша архитектура подразумевает виртуальную диспетчеризацию в производный класс из конструктора или деструктора базового класса для таких функций, как f и g, вам потребуются другие методы, например пост-конструктор — отдельная функция-член, которую вызывающий код должен явно вызвать для завершения инициализации; в ней можно безопасно вызывать f и g, поскольку в функциях-членах виртуальные вызовы работают нормально. Некоторые методы для этого показаны в ссылках ниже. Вот неполный список вариантов:
- Переложить ответственность: Просто задокументируйте, что пользовательский код должен вызвать функцию пост-инициализации сразу после создания объекта.
- Ленивая пост-инициализация: Выполняйте её при первом вызове функции-члена. Булев флаг в базовом классе указывает, была ли пост-конструкция выполнена.
- Использование семантики виртуального базового класса: Правила языка диктуют, что конструктор наиболее производного класса определяет, какой конструктор базового класса будет вызван; это можно использовать в своих интересах. (См. [\[Taligent94\]](#Taligent94).)
- Использование фабричной функции: Таким образом можно легко обеспечить обязательный вызов функции пост-конструктора.
Вот пример последнего варианта:
class B {
public:
B()
{
/* ... */
f(); // BAD: C.82: Don't call virtual functions in constructors and destructors
/* ... */
}
virtual void f() = 0;
};
class B {
protected:
class Token {};
public:
// constructor needs to be public so that make_shared can access it.
// protected access level is gained by requiring a Token.
explicit B(Token) { /* ... */ } // create an imperfectly initialized object
virtual void f() = 0;
template<class T>
static shared_ptr<T> create() // interface for creating shared objects
{
auto p = make_shared<T>(typename T::Token{});
p->post_initialize();
return p;
}
protected:
virtual void post_initialize() // called right after construction
{ /* ... */ f(); /* ... */ } // GOOD: virtual dispatch is safe
}
};
class D : public B { // some derived class
protected:
class Token {};
public:
// constructor needs to be public so that make_shared can access it.
// protected access level is gained by requiring a Token.
explicit D(Token) : B{ B::Token{} } {}
void f() override { /* ... */ };
protected:
template<class T>
friend shared_ptr<T> B::create();
};
shared_ptr<D> p = D::create<D>(); // creating a D object
Эта архитектура требует соблюдения следующих правил:
- Производные классы, такие как
D, не должны предоставлять публично вызываемый конструктор. Иначе пользователиDсмогут создавать объектыD, минуя вызовpost_initialize. - Выделение памяти ограничено
operator new. Тем не менееBможет переопределитьnew(см. пункты 45 и 46 в SuttAlex05). Dдолжен определить конструктор с теми же параметрами, которые выбралB. Однако определение нескольких перегрузокcreateможет снять эту проблему; перегрузки даже могут быть параметризованы по типам аргументов.
Если перечисленные требования выполнены, архитектура гарантирует, что post_initialize был вызван для любого полностью сконструированного объекта, производного от B. post_initialize не обязан быть виртуальным; однако он может свободно вызывать виртуальные функции.
Подводя итог: ни один метод пост-конструкции не является идеальным. Худшие методы обходят всю проблему, просто прося вызывающего вручную вызвать пост-конструктор. Даже лучшие методы требуют либо иного синтаксиса для создания объектов (легко проверить во время компиляции), либо взаимодействия со стороны авторов производных классов (невозможно проверить во время компиляции).
Ссылки: [\[Alexandrescu01\]](#Alexandrescu01) §3, [\[Boost\]](#Boost), [\[Dewhurst03\]](#Dewhurst03) §75, [\[Meyers97\]](#Meyers97) §46, [\[Stroustrup00\]](#Stroustrup00) §15.4.3, [\[Taligent94\]](#Taligent94)
Обсуждение: Делайте деструкторы базового класса публичными и виртуальными либо защищёнными и невиртуальными
Должно ли уничтожение вести себя виртуально? То есть допустимо ли уничтожение через указатель на класс base? Если да, то деструктор base должен быть публичным, чтобы его можно было вызвать, и виртуальным — иначе его вызов приведёт к неопределённому поведению. В противном случае он должен быть защищённым, чтобы только производные классы могли вызывать его в своих деструкторах, и невиртуальным, поскольку не нуждается в виртуальном поведении.
Пример
Типичный случай для базового класса — намерение иметь публично производные классы, поэтому вызывающий код практически наверняка будет использовать что-то вроде shared_ptr<base>:
class Base {
public:
~Base(); // BAD, not virtual
virtual ~Base(); // GOOD
// ...
};
class Derived : public Base { /* ... */ };
{
unique_ptr<Base> pb = make_unique<Derived>();
// ...
} // ~pb invokes correct destructor only when ~Base is virtual
В более редких случаях, например для классов-политик, класс используется как базовый для удобства, а не для полиморфного поведения. В таких случаях рекомендуется делать деструкторы защищёнными и невиртуальными:
class My_policy {
public:
virtual ~My_policy(); // BAD, public and virtual
protected:
~My_policy(); // GOOD
// ...
};
template<class Policy>
class customizable : Policy { /* ... */ }; // note: private inheritance
Примечание
Эта простая рекомендация затрагивает тонкий вопрос и отражает современные подходы к наследованию и принципам объектно-ориентированного проектирования.
Для базового класса Base вызывающий код может попытаться уничтожить производные объекты через указатели на Base, например при использовании unique_ptr<Base>. Если деструктор Base является публичным и невиртуальным (по умолчанию), его можно случайно вызвать через указатель, который фактически указывает на производный объект, — в этом случае поведение при попытке удаления не определено. Именно это обстоятельство привело к тому, что старые стандарты кодирования ввели общее требование: все деструкторы базовых классов должны быть виртуальными. Это избыточное требование (пусть и являющееся распространённой практикой); вместо этого правило должно звучать так: деструкторы базовых классов следует делать виртуальными тогда и только тогда, когда они публичны.
Написание базового класса — это определение абстракции (см. пункты 35–37). Напомним, что для каждой функции-члена, участвующей в этой абстракции, нужно решить:
- Должна ли она вести себя виртуально или нет.
- Должна ли она быть публично доступна всем вызывающим через указатель на
Base, либо она является скрытой деталью реализации.
Как описано в пункте 39, для обычной функции-члена выбор стоит между: допускать вызов через указатель на Base невиртуально (но, возможно, с виртуальным поведением, если она вызывает виртуальные функции, как в паттернах NVI или Template Method), виртуально, или вообще не допускать. Паттерн NVI — это техника избежания публичных виртуальных функций.
Уничтожение можно рассматривать как ещё одну операцию, пусть и со специальной семантикой, которая делает невиртуальные вызовы опасными или некорректными. Поэтому для деструктора базового класса выбор стоит между: допускать вызов через указатель на Base виртуально или вообще не допускать; «невиртуально» не является вариантом. Следовательно, деструктор базового класса виртуален, если его можно вызвать (т. е. он публичен), и невиртуален в противном случае.
Обратите внимание, что паттерн NVI неприменим к деструктору, поскольку конструкторы и деструкторы не могут выполнять глубокие виртуальные вызовы. (См. пункты 39 и 55.)
Следствие: при написании базового класса всегда явно объявляйте деструктор, поскольку неявно сгенерированный будет публичным и невиртуальным. Вы всегда можете задать реализацию =default, если тело по умолчанию подходит и вы пишете функцию только для придания ей нужной видимости и виртуальности.
Исключение
Некоторые компонентные архитектуры (например, COM и CORBA) не используют стандартный механизм удаления и применяют другие протоколы уничтожения объектов. Следуйте локальным паттернам и идиомам и адаптируйте данную рекомендацию по мере необходимости.
Рассмотрите также этот редкий случай:
Bодновременно является базовым классом и конкретным классом, который можно инстанцировать напрямую, — поэтому деструктор должен быть публичным, чтобы объектыBможно было создавать и уничтожать.- Тем не менее у
Bнет виртуальных функций, и он не предназначен для полиморфного использования, — поэтому, хотя деструктор и является публичным, ему не нужно быть виртуальным.
В этом случае, несмотря на то что деструктор должен быть публичным, может существовать серьёзный соблазн не делать его виртуальным — ведь как первая виртуальная функция он повлечёт накладные расходы на поддержку информации о типе во время выполнения, тогда как добавленная функциональность никогда не понадобится.
В этом редком случае можно сделать деструктор публичным и невиртуальным, но чётко задокументировать, что дальнейшие производные объекты не должны использоваться полиморфно как B. Именно так было сделано с std::unary_function.
Однако в общем случае следует избегать конкретных базовых классов (см. пункт 35). Например, unary_function — это набор typedef, который никогда не предназначался для самостоятельного инстанцирования. Не имеет смысла давать ему публичный деструктор; более правильная архитектура — следовать данному совету и дать ему защищённый невиртуальный деструктор.
Ссылки: [\[SuttAlex05\]](#SuttAlex05) Item 50, [\[Cargill92\]](#Cargill92) pp. 77-79, 207, [\[Cline99\]](#Cline99) §21.06, 21.12-13, [\[Henricson97\]](#Henricson97) pp. 110-114, [\[Koenig97\]](#Koenig97) Chapters 4, 11, [\[Meyers97\]](#Meyers97) §14, [\[Stroustrup00\]](#Stroustrup00) §12.4.2, [\[Sutter02\]](#Sutter02) §27, [\[Sutter04\]](#Sutter04) §18
Обсуждение: Использование noexcept
???
Обсуждение: Деструкторы, функции освобождения ресурсов и swap никогда не должны завершаться ошибкой
Никогда не позволяйте ошибке сообщаться из деструктора, функции освобождения ресурсов (например, operator delete) или функции swap посредством throw. Писать полезный код практически невозможно, если эти операции могут завершиться ошибкой, и даже если что-то пойдёт не так, почти никогда нет смысла повторять попытку. В частности, типы, деструкторы которых могут генерировать исключения, категорически запрещены к использованию со стандартной библиотекой C++. Большинство деструкторов теперь неявно имеют спецификацию noexcept по умолчанию.
Пример
class Nefarious {
public:
Nefarious() { /* code that could throw */ } // ok
~Nefarious() { /* code that could throw */ } // BAD, should not throw
// ...
};
- Объекты
Nefariousтрудно безопасно использовать даже как локальные переменные:
void test(string& s)
{
Nefarious n; // trouble brewing
string copy = s; // copy the string
} // destroy copy and then n
Здесь копирование `s` может выбросить исключение, и если это произойдёт, а деструктор `n` тоже выбросит исключение, программа завершится через `std::terminate`, поскольку два исключения не могут распространяться одновременно.
- Классы с элементами или базовыми классами типа
Nefariousтакже трудно безопасно использовать, поскольку их деструкторы должны вызывать деструкторNefariousи точно так же отравлены его плохим поведением:
class Innocent_bystander {
Nefarious member; // oops, poisons the enclosing class's destructor
// ...
};
void test(string& s)
{
Innocent_bystander i; // more trouble brewing
string copy2 = s; // copy the string
} // destroy copy and then i
Здесь, если при конструировании `copy2` будет выброшено исключение, возникнет та же проблема, потому что деструктор `i` теперь тоже может выбросить исключение, и тогда будет вызван `std::terminate`.
- Нельзя надёжно создавать глобальные или статические объекты
Nefarious:
static Nefarious n; // oops, any destructor exception can't be caught
- Нельзя надёжно создавать массивы объектов
Nefarious:
void test()
{
std::array<Nefarious, 10> arr; // this line can std::terminate()
}
Поведение массивов не определено при наличии деструкторов, которые выбрасывают исключения, поскольку невозможно придумать разумную стратегию отката. Просто подумайте: какой код может сгенерировать компилятор для конструирования `arr`, если конструктор четвёртого объекта выбросит исключение и в режиме очистки код попытается вызвать деструкторы уже сконструированных объектов... а один или несколько из этих деструкторов выбросят исключение? Удовлетворительного ответа нет.
- Нельзя использовать объекты
Nefariousв стандартных контейнерах:
std::vector<Nefarious> vec(10); // this line can std::terminate()
Стандартная библиотека запрещает все используемые с ней деструкторы выбрасывать исключения. Нельзя хранить объекты `Nefarious` в стандартных контейнерах или использовать их с любой другой частью стандартной библиотеки.
Примечание
Это ключевые функции, которые не должны завершаться ошибкой, поскольку они необходимы для двух ключевых операций в транзакционном программировании: отката работы при возникновении проблем в процессе обработки и фиксации работы при отсутствии проблем. Если нет способа безопасно откатить изменения с помощью операций, не допускающих ошибок, реализовать откат без ошибок невозможно. Если нет способа безопасно зафиксировать изменения состояния с помощью операций, не допускающих ошибок (прежде всего, но не ограничиваясь, swap), реализовать фиксацию без ошибок невозможно.
Рассмотрите следующие советы и требования, содержащиеся в стандарте C++:
If a destructor called during stack unwinding exits with an exception, terminate is called (15.5.1). So destructors should generally catch exceptions and not let them propagate out of the destructor. --[\[C++03\]](#Cplusplus03) §15.2(3) No destructor operation defined in the C++ Standard Library (including the destructor of any type that is used to instantiate a standard-library template) will throw an exception. --[\[C++03\]](#Cplusplus03) §17.4.4.8(3)
Функции освобождения памяти, в частности перегруженные operator delete и operator delete[], относятся к той же категории, поскольку они также используются при общей очистке и при обработке исключений для отката частично выполненной работы. Помимо деструкторов и функций освобождения памяти, распространённые техники обеспечения безопасности при работе с ошибками также опираются на то, что операции swap никогда не завершаются ошибкой, — в данном случае не потому, что они используются для реализации гарантированного отката, а потому что они используются для реализации гарантированной фиксации. Например, вот идиоматическая реализация operator= для типа T, выполняющая копирующее конструирование с последующим вызовом swap без ошибок:
T& T::operator=(const T& other)
{
auto temp = other;
swap(temp);
return *this;
}
(См. также пункт 56. ???)
К счастью, при освобождении ресурса область возможных ошибок определённо меньше. Если вы используете исключения как механизм сообщения об ошибках, убедитесь, что такие функции обрабатывают все исключения и прочие ошибки, которые могут возникнуть в ходе их внутренней работы. (Для исключений достаточно обернуть всё уязвимое в вашем деструкторе в блок try/catch(...). ) Это особенно важно, поскольку деструктор может быть вызван в кризисной ситуации, например при сбое выделения системного ресурса (памяти, файлов, блокировок, портов, окон или других системных объектов).
Если вы используете исключения как механизм обработки ошибок, всегда документируйте это поведение, объявляя такие функции с noexcept. (См. пункт 75.)
Ссылки: [\[SuttAlex05\]](#SuttAlex05) Item 51; [\[C++03\]](#Cplusplus03) §15.2(3), §17.4.4.8(3), [\[Meyers96\]](#Meyers96) §11, [\[Stroustrup00\]](#Stroustrup00) §14.4.7, §E.2-4, [\[Sutter00\]](#Sutter00) §8, §16, [\[Sutter02\]](#Sutter02) §18-19
Определяйте копирование, перемещение и уничтожение согласованно
Причина
???
Примечание
Если вы определяете конструктор копирования, вы также должны определить оператор присваивания копированием.
Примечание
Если вы определяете конструктор перемещения, вы также должны определить оператор присваивания перемещением.
Пример
class X {
public:
X(const X&) { /* stuff */ }
// BAD: failed to also define a copy assignment operator
X(x&&) noexcept { /* stuff */ }
// BAD: failed to also define a move assignment operator
// ...
};
X x1;
X x2 = x1; // ok
x2 = x1; // pitfall: either fails to compile, or does something suspicious
Если вы определяете деструктор, не следует использовать сгенерированные компилятором операции копирования или перемещения; вероятно, вам потребуется определить или запретить копирование и/или перемещение.
class X {
HANDLE hnd;
// ...
public:
~X() { /* custom stuff, such as closing hnd */ }
// suspicious: no mention of copying or moving -- what happens to hnd?
};
X x1;
X x2 = x1; // pitfall: either fails to compile, or does something suspicious
x2 = x1; // pitfall: either fails to compile, or does something suspicious
Если вы определяете копирование, и любой базовый класс или элемент имеет тип, определяющий операцию перемещения, вы также должны определить операцию перемещения.
class X {
string s; // defines more efficient move operations
// ... other data members ...
public:
X(const X&) { /* stuff */ }
X& operator=(const X&) { /* stuff */ }
// BAD: failed to also define a move construction and move assignment
// (why wasn't the custom "stuff" repeated here?)
};
X test()
{
X local;
// ...
return local; // pitfall: will be inefficient and/or do the wrong thing
}
Если вы определяете любую из пяти функций — конструктор копирования, оператор присваивания копированием или деструктор, — вероятно, следует определить и остальные.
Примечание
Если вам нужно определить любую из этих пяти функций, значит, вам нужно, чтобы она делала больше, чем поведение по умолчанию, — а эти пять функций асимметрично взаимосвязаны. Вот как:
- Если вы пишете или запрещаете конструктор копирования или оператор присваивания копированием, вероятно, нужно сделать то же самое и для другой: если одна выполняет «особую» работу, то, скорее всего, и другая должна, потому что обе функции должны иметь схожий эффект. (См. пункт 53, в котором этот вопрос рассматривается подробнее.)
- Если вы явно пишете функции копирования, вероятно, нужно написать и деструктор: если «особая» работа в конструкторе копирования состоит в выделении или дублировании какого-либо ресурса (памяти, файла, сокета и т. п.), вам нужно освободить его в деструкторе.
- Если вы явно пишете деструктор, вероятно, нужно явно написать или запретить копирование: если вам приходится писать нетривиальный деструктор, это нередко означает, что вам нужно вручную освобождать ресурс, которым владеет объект. В таком случае эти ресурсы, скорее всего, требуют тщательного дублирования, и тогда нужно уделить внимание способу копирования и присваивания объектов или полностью запретить копирование.
Во многих случаях хранение ресурсов в должным образом инкапсулированных RAII-объектах-«владельцах» позволяет устранить необходимость писать эти операции самостоятельно. (См. пункт 13.)
Отдавайте предпочтение специальным функциям, сгенерированным компилятором (включая =default); только они могут быть классифицированы как «тривиальные», и как минимум один крупный поставщик стандартных библиотек активно оптимизирует классы с тривиальными специальными функциями. Вероятно, это станет общепринятой практикой.
Исключения: Если какие-либо специальные функции объявлены только для того, чтобы сделать их не-публичными или виртуальными, без специальной семантики, это не подразумевает необходимости в других. В редких случаях классы с элементами странных типов (например, элементами-ссылками) являются исключением, поскольку имеют своеобразную семантику копирования. В классе, содержащем ссылку, вероятно, нужно написать конструктор копирования и оператор присваивания, но деструктор по умолчанию уже делает правильное. (Обратите внимание, что использование элемента-ссылки почти всегда является ошибкой.)
Ссылки: [\[SuttAlex05\]](#SuttAlex05) Item 52; [\[Cline99\]](#Cline99) §30.01-14, [\[Koenig97\]](#Koenig97) §4, [\[Stroustrup00\]](#Stroustrup00) §5.5, §10.4, [\[SuttHysl04b\]](#SuttHysl04b)
Сводка правил управления ресурсами:
- Обеспечивайте строгую безопасность ресурсов; то есть никогда не допускайте утечки того, что вы считаете ресурсом
- Никогда не возвращайте управление и не выбрасывайте исключение, удерживая ресурс, которым не владеет дескриптор
- «Сырой» указатель или ссылка никогда не являются дескриптором ресурса
- Никогда не позволяйте указателю пережить объект, на который он указывает
- Используйте шаблоны для представления контейнеров (и других дескрипторов ресурсов)
- Возвращайте контейнеры по значению (полагаясь на перемещение или оптимизацию копирования для эффективности)
- Если класс является дескриптором ресурса, ему нужны конструктор, деструктор и операции копирования и/или перемещения
- Если класс является контейнером, дайте ему конструктор со списком инициализаторов
Обсуждение: Обеспечивайте строгую безопасность ресурсов; то есть никогда не допускайте утечки того, что вы считаете ресурсом
Причина
Предотвращение утечек. Утечки могут привести к деградации производительности, загадочным ошибкам, сбоям системы и нарушениям безопасности.
Альтернативная формулировка: Пусть каждый ресурс будет представлен объектом некоторого класса, управляющего его временем жизни.
Пример
template<class T>
class Vector {
private:
T* elem; // sz elements on the free store, owned by the class object
int sz;
// ...
};
Этот класс является дескриптором ресурса. Он управляет временем жизни объектов T. Для этого Vector должен определить или запретить операции копирования, перемещения и уничтожения.
Пример
??? "odd" non-memory resource ???
Применение
Основная техника предотвращения утечек — обеспечить, чтобы каждый ресурс принадлежал дескриптору ресурса с подходящим деструктором. Анализатор может обнаруживать «голые new». Имея список функций выделения памяти в стиле C (например, fopen()), анализатор также может выявлять использование, не управляемое дескриптором ресурса. В целом «голые указатели» могут рассматриваться с подозрением, помечаться и/или анализироваться. Полный список ресурсов не может быть составлен без участия человека (определение «ресурса» по необходимости слишком общее), но инструмент можно «параметризовать» списком ресурсов.
Обсуждение: Никогда не возвращайте управление и не выбрасывайте исключение, удерживая ресурс, которым не владеет дескриптор
Причина
Это приведёт к утечке.
Пример
void f(int i)
{
FILE* f = fopen("a file", "r");
ifstream is { "another file" };
// ...
if (i == 0) return;
// ...
fclose(f);
}
Если i == 0, файловый дескриптор для a file утекает. С другой стороны, ifstream для another file корректно закроет свой файл (при уничтожении). Если необходимо использовать явный указатель, а не дескриптор ресурса с конкретной семантикой, используйте unique_ptr или shared_ptr с пользовательским делитером:
void f(int i)
{
unique_ptr<FILE, int(*)(FILE*)> f(fopen("a file", "r"), fclose);
// ...
if (i == 0) return;
// ...
}
Лучше:
void f(int i)
{
ifstream input {"a file"};
// ...
if (i == 0) return;
// ...
}
Применение
Анализатор должен считать все «голые указатели» подозрительными. Анализатор, вероятно, должен опираться на предоставленный человеком список ресурсов. ля начала нам известны стандартные контейнеры библиотеки, string и умные указатели. Использование span и string_view должно существенно помочь (они не являются дескрипторами ресурсов).
Обсуждение: «Сырой» указатель или ссылка никогда не являются дескриптором ресурса
Причина
Чтобы иметь возможность различать владельцев и наблюдателей.
Примечание
Это не зависит от того, как вы «записываете» указатель: T*, T&, Ptr<T> и Range<T> не являются владельцами.
Обсуждение: Никогда не позволяйте указателю пережить объект, на который он указывает
Причина
Чтобы избежать крайне трудно обнаруживаемых ошибок. Разыменование такого указателя является неопределённым поведением и может привести к нарушениям системы типов.
Пример
string* bad() // really bad
{
vector<string> v = { "This", "will", "cause", "trouble", "!" };
// leaking a pointer into a destroyed member of a destroyed object (v)
return &v[0];
}
void use()
{
string* p = bad();
vector<int> xx = {7, 8, 9};
// undefined behavior: x might not be the string "This"
string x = *p;
// undefined behavior: we don't know what (if anything) is allocated a location p
*p = "Evil!";
}
Строки v уничтожаются при выходе из bad(), как и сам v. Возвращённый указатель указывает на неразмещённую память в хипе. Эта память (на которую указывает p) к моменту выполнения *p могла быть повторно выделена. Строки для чтения может не оказаться, а запись через p может легко испортить объекты несвязанных типов.
Применение
Большинство компиляторов уже предупреждают о простых случаях и располагают информацией для более глубокого анализа. Считайте любой указатель, возвращаемый из функции, подозрительным. Используйте контейнеры, дескрипторы ресурсов и представления (например, span, про который известно, что он не является дескриптором ресурса), чтобы сократить число случаев для проверки. Для начала считайте каждый класс с деструктором дескриптором ресурса.
Обсуждение: Используйте шаблоны для представления контейнеров (и других дескрипторов ресурсов)
Причина
Чтобы обеспечить статически типобезопасное манипулирование элементами.
Пример
template<typename T> class Vector {
// ...
T* elem; // point to sz elements of type T
int sz;
};
Обсуждение: Возвращайте контейнеры по значению (полагаясь на перемещение или оптимизацию копирования для эффективности)
Причина
Чтобы упростить код и устранить необходимость в явном управлении памятью. Чтобы ввести объект в окружающую область видимости, тем самым продлив его время жизни.
См. также: F.20, общий пункт о «выходных» выходных значениях
Пример
vector<int> get_large_vector()
{
return ...;
}
auto v = get_large_vector(); // return by value is ok, most modern compilers will do copy elision
Исключение
См. исключения в F.20.
Применение
Проверяйте указатели и ссылки, возвращаемые из функций, и смотрите, присваиваются ли они дескрипторам ресурсов (например, unique_ptr).
Обсуждение: Если класс является дескриптором ресурса, ему нужны конструктор, деструктор и операции копирования и/или перемещения
Причина
Чтобы обеспечить полный контроль над временем жизни ресурса. Чтобы предоставить согласованный набор операций над ресурсом.
Пример
??? Messing with pointers
Примечание
Если все элементы являются дескрипторами ресурсов, по возможности полагайтесь на сгенерированные компилятором операции.
template<typename T> struct Named {
string name;
T value;
};
Теперь Named имеет конструктор по умолчанию, деструктор, а также эффективные операции копирования и перемещения при условии, что T также имеет их.
Применение
В целом инструмент не может знать, является ли класс дескриптором ресурса. Однако если класс имеет некоторые из операций по умолчанию, он должен иметь все, и если у класса есть элемент, являющийся дескриптором ресурса, он сам должен рассматриваться как дескриптор ресурса.
Обсуждение: Если класс является контейнером, дайте ему конструктор со списком инициализаторов
Причина
Обычно требуется начальный набор элементов.
Пример
template<typename T> class Vector {
public:
Vector(std::initializer_list<T>);
// ...
};
Vector<string> vs { "Nygaard", "Ritchie" };
Применение
Когда класс является контейнером? ???
Приложение D: Вспомогательные инструменты
В этом разделе приведён список инструментов, непосредственно поддерживающих внедрение C++ Core Guidelines. Этот список не является исчерпывающим перечнем инструментов, полезных при написании хорошего кода на C++. Если инструмент специально разработан для поддержки C++ Core Guidelines и ссылается на них, он является кандидатом для включения в список.
Инструменты: Clang-tidy
Clang-tidy содержит набор правил, специально обеспечивающих соблюдение C++ Core Guidelines. Эти правила названы по шаблону cppcoreguidelines-*.
Инструменты: CppCoreCheck
Анализ кода C++ в компиляторе Microsoft содержит набор правил, специально направленных на обеспечение соблюдения C++ Core Guidelines.
Глоссарий
Относительно неформальные определения терминов, используемых в руководстве (основаны на глоссарии в Programming: Principles and Practice using C++)
Дополнительную информацию по многим темам, связанным с C++, можно найти на сайте Standard C++ Foundation.
Класс становится абстрактным при наличии чистой виртуальной функции или только защищённых конструкторов.
Нередко приближение является результатом компромисса между идеалами.
Иногда термин «сложность» используется (упрощённо) как оценка числа операций, необходимых для выполнения алгоритма.
Как правило, конструктор устанавливает инвариант и нередко захватывает ресурсы, необходимые объекту для использования (которые затем обычно освобождаются деструктором).
К сожалению, спецификация может быть неполной или противоречивой либо не отвечать разумным ожиданиям пользователей. Поэтому для получения приемлемого кода иногда нужно делать больше, чем просто следовать формальной спецификации.
В идеале стоимость должна быть функцией сложности.
Упрощённое определение: объявление, выделяющее память.
Обобщённый алгоритм будет работать для всех типов аргументов, удовлетворяющих его требованиям. В C++ обобщённое программирование, как правило, использует шаблоны.
Например, имя из вложенной (внутренней) области видимости может скрывать то же имя из внешней (охватывающей) области видимости.
На практике такая рекурсия никогда не бывает бесконечной, а прерывается каким-либо аппаратным сбоем.
В частности, объект регулярного типа может быть скопирован, и результат копирования является отдельным объектом, сравнивающимся как равный исходному. См. также semiregular type (полурегулярный тип).
- ABI: Application Binary Interface — спецификация для конкретной аппаратной платформы в сочетании с операционной системой. Ср. с API.
- abstract class (абстрактный класс): класс, который не может быть напрямую использован для создания объектов; часто применяется для определения интерфейса для производных классов.
- abstraction (абстракция): описание чего-либо, которое избирательно и намеренно игнорирует (скрывает) детали (например, детали реализации); избирательное незнание.
- address (адрес): значение, позволяющее найти объект в памяти компьютера.
- algorithm (алгоритм): процедура или формула для решения задачи; конечная последовательность вычислительных шагов для получения результата.
- alias (псевдоним): альтернативный способ обращения к объекту; нередко имя, указатель или ссылка.
- API: Application Programming Interface — набор функций, обеспечивающих взаимодействие между различными программными компонентами. Ср. с ABI.
- application (приложение): программа или набор программ, рассматриваемых пользователями как единое целое.
- approximation (приближение): нечто (например, значение или архитектура), близкое к идеальному (значению или архитектуре).
- argument (аргумент): значение, передаваемое функции или шаблону, где оно доступно через параметр.
- array (массив): однородная последовательность элементов, обычно нумерованных, например
[0:max). - assertion (утверждение): оператор, вставленный в программу для объявления (утверждения) о том, что в данной точке программы нечто всегда должно быть истинным.
- base class (базовый класс): тип, предназначенный для производного использования (например, имеющий не-
finalвиртуальную функцию), причём объекты этого типа предназначены для использования только косвенно (например, через указатель). \[В строгом смысле «базовый класс» можно определить как «то, от чего мы наследуем», но мы формулируем это в терминах намерений проектировщика класса.\] Как правило, базовый класс имеет одну или несколько виртуальных функций. - bit (бит): базовая единица информации в компьютере. Бит может иметь значение 0 или 1.
- bug (баг): ошибка в программе.
- byte (байт): базовая единица адресации в большинстве компьютеров. Как правило, байт содержит 8 бит.
- class (класс): определяемый пользователем тип, который может содержать элементы-данные, функции-члены и типы-члены.
- code (код): программа или её часть; термин неоднозначно используется как для исходного кода, так и для объектного кода.
- compiler (компилятор): программа, преобразующая исходный код в объектный код.
- complexity (сложность): трудно точно определяемое понятие или мера трудности построения решения задачи или самого решения.
- computation (вычисление): выполнение некоторого кода, обычно принимающего какой-либо входной сигнал и производящего некоторый выходной.
- concept (концепция): (1) понятие, идея; (2) набор требований, обычно к аргументу шаблона.
- concrete type (конкретный тип): тип, не являющийся базовым классом; объекты этого типа предназначены для непосредственного использования (не только через указатель/косвенно), его размер известен, он, как правило, может быть размещён там, где программист считает нужным (например, на стеке или статически).
- constant (константа): значение, которое не может быть изменено (в данной области видимости); не изменяемое.
- constructor (конструктор): операция, инициализирующая («конструирующая») объект.
- container (контейнер): объект, содержащий элементы (другие объекты).
- copy (копирование): операция, в результате которой два объекта получают значения, сравнивающиеся как равные. См. также move (перемещение).
- correctness (корректность): программа или часть программы является корректной, если она соответствует своей спецификации.
- cost (стоимость): затраты (например, время программиста, время выполнения или память) на создание программы или её исполнение.
- customization point (точка кастомизации): ???
- data (данные): значения, используемые при вычислениях.
- debugging (отладка): процесс поиска и устранения ошибок в программе; как правило, значительно менее систематичен, чем тестирование.
- declaration (объявление): спецификация имени с его типом в программе.
- definition (определение): объявление сущности, предоставляющее всю информацию, необходимую для завершения программы с использованием этой сущности.
- derived class (производный класс): класс, производный от одного или нескольких базовых классов.
- design (архитектура/проектирование): общее описание того, как программный продукт должен работать для соответствия своей спецификации.
- destructor (деструктор): операция, неявно вызываемая при уничтожении объекта (например, в конце области видимости). Нередко освобождает ресурсы.
- encapsulation (инкапсуляция): защита того, что должно оставаться закрытым (например, деталей реализации), от несанкционированного доступа.
- error (ошибка): несоответствие между разумными ожиданиями от поведения программы (часто выраженными в виде требований или руководства пользователя) и тем, что программа реально делает.
- executable (исполняемый файл): программа, готовая к запуску (исполнению) на компьютере.
- feature creep (разрастание функциональности): тенденция добавлять избыточную функциональность в программу «на всякий случай».
- file (файл): контейнер постоянной информации в компьютере.
- floating-point number (число с плавающей точкой): приближение вещественного числа в компьютере, например 7.93 и 10.78e-3.
- function (функция): именованная единица кода, которую можно вызывать из разных частей программы; логическая единица вычисления.
- generic programming (обобщённое программирование): стиль программирования, ориентированный на проектирование и эффективную реализацию алгоритмов.
- global variable (глобальная переменная): технически — именованный объект в области видимости пространства имён.
- handle (дескриптор): класс, предоставляющий доступ к другому объекту через указатель-член или ссылку. См. также resource (ресурс), copy (копирование), move (перемещение).
- header (заголовочный файл): файл, содержащий объявления для совместного использования интерфейсов между частями программы.
- hiding (сокрытие): действие, предотвращающее непосредственное видение или доступ к фрагменту информации.
- ideal (идеал): идеальная версия чего-либо, к чему мы стремимся. Как правило, приходится идти на компромисс и довольствоваться приближением.
- implementation (реализация): (1) процесс написания и тестирования кода; (2) код, реализующий программу.
- infinite loop (бесконечный цикл): цикл, условие завершения которого никогда не становится истинным. См. iteration (итерация).
- infinite recursion (бесконечная рекурсия): рекурсия, не завершающаяся до тех пор, пока машина не исчерпает память для хранения вызовов.
- information hiding (сокрытие информации): разделение интерфейса и реализации с целью скрыть детали реализации, не предназначенные для внимания пользователя, и предоставить абстракцию.
- initialize (инициализировать): присвоить объекту его первое (начальное) значение.
- input (ввод): значения, используемые при вычислениях (например, аргументы функции и символы, введённые с клавиатуры).
- integer (целое число): целое число, например 42 и -99.
- interface (интерфейс): объявление или набор объявлений, определяющих, как можно вызвать фрагмент кода (например, функцию или класс).
- invariant (инвариант): нечто, что всегда должно быть истинным в данной точке (или точках) программы; обычно используется для описания состояния (набора значений) объекта или состояния цикла перед входом в повторяющийся оператор.
- iteration (итерация): многократное выполнение фрагмента кода; см. recursion (рекурсия).
- iterator (итератор): объект, идентифицирующий элемент последовательности.
- ISO: International Organization for Standardization (Международная организация по стандартизации). Язык C++ является стандартом ISO, ISO/IEC 14882. Дополнительная информация на iso.org.
- library (библиотека): набор типов, функций, классов и т. д., реализующих набор средств (абстракций), предназначенных для потенциального использования в более чем одной программе.
- lifetime (время жизни): период от инициализации объекта до момента, когда он становится непригодным для использования (выходит из области видимости, удаляется или программа завершается).
- linker (компоновщик): программа, объединяющая объектные файлы и библиотеки в исполняемую программу.
- literal (литерал): нотация, непосредственно задающая значение, например 12, задающее целочисленное значение «двенадцать».
- loop (цикл): фрагмент кода, выполняемый многократно; в C++ обычно оператор for или оператор
while. - move (перемещение): операция, передающая значение из одного объекта в другой, оставляя в первом значение, представляющее «пустоту». См. также copy (копирование).
- move-only type (тип только для перемещения): конкретный тип, который является перемещаемым, но не копируемым.
- mutable (изменяемый): допускающий изменение; противоположность неизменяемому, константному и инвариантному.
- object (объект): (1) инициализированная область памяти известного типа, содержащая значение этого типа; (2) область памяти.
- object code (объектный код): вывод компилятора, предназначенный в качестве ввода для компоновщика (чтобы компоновщик создал исполняемый код).
- object file (объектный файл): файл, содержащий объектный код.
- object-oriented programming (объектно-ориентированное программирование, OOP): стиль программирования, ориентированный на проектирование и использование классов и иерархий классов.
- operation (операция): нечто, способное выполнять некоторое действие, например функция и оператор.
- output (вывод): значения, получаемые в результате вычислений (например, результат функции или строки символов, выводимые на экран).
- overflow (переполнение): получение значения, которое не может быть сохранено в целевом хранилище.
- overload (перегрузка): определение двух функций или операторов с одинаковым именем, но разными типами аргументов (операндов).
- override (переопределение): определение функции в производном классе с тем же именем и типами аргументов, что и виртуальная функция в базовом классе, что делает функцию вызываемой через интерфейс, определённый базовым классом.
- owner (владелец): объект, ответственный за освобождение ресурса.
- paradigm (парадигма): несколько претенциозный термин для обозначения стиля проектирования или программирования; нередко используется с (ошибочным) подразумеванием, что существует парадигма, превосходящая все остальные.
- parameter (параметр): объявление явного входного значения функции или шаблона. При вызове функция может обращаться к переданным аргументам через имена своих параметров.
- pointer (указатель): (1) значение, используемое для идентификации типизированного объекта в памяти; (2) переменная, содержащая такое значение.
- post-condition (постусловие): условие, которое должно выполняться при выходе из фрагмента кода, например функции или цикла.
- pre-condition (предусловие): условие, которое должно выполняться при входе в фрагмент кода, например функцию или цикл.
- program (программа): код (возможно, с сопутствующими данными), достаточно полный для выполнения на компьютере.
- programming (программирование): искусство выражения решений задач в виде кода.
- programming language (язык программирования): язык для записи программ.
- pseudo code (псевдокод): описание вычисления, записанное в неформальной нотации, а не на языке программирования.
- pure virtual function (чистая виртуальная функция): виртуальная функция, которая должна быть переопределена в производном классе.
- RAII: («Resource Acquisition Is Initialization» — «получение ресурса есть инициализация») основная техника управления ресурсами, основанная на областях видимости.
- range (диапазон): последовательность значений, которая может быть описана начальной и конечной точкой. Например,
[0:5)означает значения 0, 1, 2, 3 и 4. - recursion (рекурсия): вызов функцией самой себя; см. также iteration (итерация).
- reference (ссылка): (1) значение, описывающее местоположение типизированного значения в памяти; (2) переменная, содержащая такое значение.
- regular expression (регулярное выражение): нотация для шаблонов в строках символов.
- regular (регулярный): полурегулярный тип, допускающий сравнение на равенство (см. концепцию
std::regular). После копирования скопированный объект сравнивается как равный исходному. Регулярный тип ведёт себя аналогично встроенным типам, таким какint, и может сравниваться с помощью==. - requirement (требование): (1) описание желаемого поведения программы или её части; (2) описание предположений, которые функция или шаблон предъявляет к своим аргументам.
- resource (ресурс): нечто, что захватывается и должно быть впоследствии освобождено, например файловый дескриптор, блокировка или память. См. также handle (дескриптор), owner (владелец).
- rounding (округление): преобразование значения к математически ближайшему значению типа с меньшей точностью.
- RTTI: Run-Time Type Information (информация о типе во время выполнения). ???
- scope (область видимости): область текста программы (исходного кода), в которой на имя можно ссылаться.
- semiregular (полурегулярный): конкретный тип, который является копируемым (включая перемещение) и конструируемым по умолчанию (см. концепцию
std::semiregular). Результат копирования является независимым объектом с тем же значением, что и исходный. Полурегулярный тип ведёт себя примерно как встроенный тип, такой какint, но, возможно, без оператора==. См. также regular type (регулярный тип). - sequence (последовательность): элементы, которые можно обходить в линейном порядке.
- software (программное обеспечение): набор фрагментов кода и сопутствующих данных; нередко используется взаимозаменяемо с «программой».
- source code (исходный код): код, созданный программистом и (в принципе) читаемый другими программистами.
- source file (исходный файл): файл, содержащий исходный код.
- specification (спецификация): описание того, что должен делать фрагмент кода.
- standard (стандарт): официально согласованное определение чего-либо, например языка программирования.
- state (состояние): набор значений.
- STL: часть стандартной библиотеки, включающая контейнеры, итераторы и алгоритмы.
- string (строка): последовательность символов.
- style (стиль): набор техник программирования, ведущих к последовательному использованию возможностей языка; иногда используется в очень узком смысле, обозначая лишь низкоуровневые правила именования и оформления кода.
- subtype (подтип): производный тип; тип, обладающий всеми свойствами некоторого типа и, возможно, дополнительными.
- supertype (надтип): базовый тип; тип, обладающий подмножеством свойств некоторого типа.
- system (система): (1) программа или набор программ для выполнения задачи на компьютере; (2) сокращение от «операционная система» — основная среда выполнения и инструменты для компьютера.
- TS: Technical Specification (Техническая спецификация). Техническая спецификация посвящена работам, ещё находящимся в стадии технической разработки, или тем случаям, когда есть вероятность, что в будущем (но не в ближайшее время) возможно достижение согласия по Международному стандарту. Техническая спецификация публикуется для немедленного использования и также служит средством получения обратной связи. Цель состоит в том, что со временем она будет преобразована и переиздана в виде Международного стандарта.
- template (шаблон): класс или функция, параметризованные одним или несколькими типами или (компиляционными) значениями; базовая конструкция языка C++, поддерживающая обобщённое программирование.
- testing (тестирование): систематический поиск ошибок в программе.
- trade-off (компромисс): результат балансирования нескольких критериев проектирования и реализации.
- truncation (усечение): потеря информации при преобразовании значения в тип, который не может точно представить преобразуемое значение.
- type (тип): нечто, определяющее набор возможных значений и набор операций для объекта.
- uninitialized (неинициализированный): (неопределённое) состояние объекта до его инициализации.
- unit (единица): (1) стандартная мера, придающая смысл значению (например, км для расстояния); (2) выделенная (например, именованная) часть более крупного целого.
- use case (вариант использования): конкретный (как правило, простой) способ использования программы, предназначенный для проверки её функциональности и демонстрации её назначения.
- value (значение): набор битов в памяти, интерпретируемых в соответствии с типом.
- value type (тип-значение): термин, используемый некоторыми для обозначения регулярного или полурегулярного типа.
- variable (переменная): именованный объект заданного типа; содержит значение, если не является неинициализированным.
- virtual function (виртуальная функция): функция-член, которая может быть переопределена в производном классе.
- word (слово): базовая единица памяти в компьютере, нередко единица, используемая для хранения целого числа.
Список задач: Неклассифицированные протоправила
Это наш список задач. В конечном счёте записи станут правилами или частью правил. Как вариант, мы решим, что изменения не нужны, и удалим запись.
- Никаких дружественных связей на большие расстояния
- Следует ли затрагивать физическую архитектуру (что в каком файле) и крупномасштабное проектирование (библиотеки, группы библиотек)?
- Пространства имён
- Избегайте директив using в глобальной области видимости (кроме std и других «фундаментальных» пространств имён, например experimental)
- Насколько детальными должны быть пространства имён? Все классы/функции, предназначенные для совместной работы и выпускаемые вместе (как определено в Sutter/Alexandrescu), или что-то более узкое или широкое?
- Следует ли использовать встроенные пространства имён (наподобие
std::literals::*_literals)? - Избегайте неявных преобразований
- Константные функции-члены должны быть потокобезопасны ... то есть, я на самом деле не изменяю переменную, просто присваиваю ей значение при первом вызове ... эх
- Всегда инициализируйте переменные, используйте списки инициализации для элементов данных.
- Любой, кто пишет публичный интерфейс, принимающий или возвращающий
void*, заслуживает сурового порицания. Это одна из моих любимых мозолей уже много лет. :) - Используйте
constвезде, где возможно: в функциях-членах, переменных и (ура)const_iterators - Используйте
auto (size)vs.{initializers}vs.{Extent{size}}- Не злоупотребляйте абстракцией
- Никогда не передавайте указатель вниз по стеку вызовов
- падение сквозь дно функции
- Следует ли включить рекомендации по выбору между видами полиморфизма? ДА. Классический (виртуальные функции, референтная семантика) vs. стиль Sean Parent (семантика значений, стирание типов, похоже на
std::function) vs. CRTP/статический? ДА Возможно, даже vs. диспетчеризация по тегам? - Следует ли запрещать виртуальные вызовы из конструкторов/деструкторов в ваших рекомендациях? ДА. Многие их запрещают, хотя я думаю, что это большое преимущество C++, что они ??? -сохраняющие (D меня так разочаровал, когда пошёл по пути Java). КАКОЙ БЫ ХОРОШИЙ ПРИМЕР ПОДОБРАТЬ?
- Если говорить о лямбдах, что может повлиять на выбор между лямбдами и (локальными?) классами в вызовах алгоритмов и других сценариях с обратными вызовами?
- И если говорить о
std::bind, Stephen T. Lavavej критикует его настолько, что я начинаю задаваться вопросом, не исчезнет ли он в будущем. Следует ли вместо него рекомендовать лямбды? - Что делать с утечками из временных объектов?:
p = (s1 + s2).c_str(); - инвалидация указателей/итераторов, ведущая к висячим указателям:
void bad()
{
int* p = new int[700];
int* q = &p[7];
delete p;
vector<int> v(700);
int* q2 = &v[7];
v.resize(900);
// ... use q and q2 ...
}
- LSP
- приватное наследование vs./и членство
- избегайте статических переменных-членов класса (гонки данных, почти-глобальные переменные)
- Используйте RAII-охранники блокировок (
lock_guard,unique_lock,shared_lock), никогда не вызывайтеmutex.lockиmutex.unlockнапрямую (RAII) - Предпочитайте нерекурсивные блокировки (нередко используются как обходной путь для плохо продуманного кода, создают накладные расходы)
- Присоединяйте свои потоки! (из-за
std::terminateв деструкторе, если поток не присоединён и не отделён... есть ли веская причина отделять потоки?) -- ??? может ли вспомогательная библиотека предоставить RAII-обёртку дляstd::thread? - Если необходимо захватить два или более мьютекса одновременно, используйте
std::lock(или другой алгоритм предотвращения взаимных блокировок?) - При использовании
condition_variableвсегда защищайте условие мьютексом (атомарный bool, значение которого устанавливается за пределами мьютекса, — неверно!), и используйте один и тот же мьютекс для самой условной переменной. - Никогда не используйте
atomic_compare_exchange_strongсstd::atomic<user-defined-struct>(различия в выравнивании имеют значение, тогда какcompare_exchange_weakв цикле сходится к стабильному выравниванию) - отдельные объекты
shared_futureне являются потокобезопасными: два потока не могут ждать на одном и том же объектеshared_future(они могут ждать на копияхshared_future, ссылающихся на одно и то же разделяемое состояние) - отдельные объекты
shared_ptrне являются потокобезопасными: разные потоки могут вызывать не-constфункции-члены на разныхshared_ptr, ссылающихся на один и тот же разделяемый объект, но один поток не может вызывать не-constфункцию-член объектаshared_ptr, пока другой поток обращается к тому же объектуshared_ptr(если это требуется, рассмотрите использованиеatomic_shared_ptr)
- правила арифметики
Библиография
\[Abrahams01]: D. Abrahams. Exception-Safety in Generic Components.
\[Alexandrescu01]: A. Alexandrescu. Modern C++ Design (Addison-Wesley, 2001).
\[C++03]: ISO/IEC 14882:2003(E), Programming Languages — C++ (обновлённый стандарт ISO и ANSI C++, включающий содержимое (C++98) плюс исправления ошибок).
\[Cargill92]: T. Cargill. C++ Programming Style (Addison-Wesley, 1992).
\[Cline99]: M. Cline, G. Lomow, and M. Girou. C++ FAQs (2ndEdition) (Addison-Wesley, 1999).
\[Dewhurst03]: S. Dewhurst. C++ Gotchas (Addison-Wesley, 2003).
\[Henricson97]: M. Henricson and E. Nyquist. Industrial Strength C++ (Prentice Hall, 1997).
\[Koenig97]: A. Koenig and B. Moo. Ruminations on C++ (Addison-Wesley, 1997).
\[Lakos96]: J. Lakos. Large-Scale C++ Software Design (Addison-Wesley, 1996).
\[Meyers96]: S. Meyers. More Effective C++ (Addison-Wesley, 1996).
\[Meyers97]: S. Meyers. Effective C++ (2nd Edition) (Addison-Wesley, 1997).
\[Meyers01]: S. Meyers. Effective STL (Addison-Wesley, 2001).
\[Meyers05]: S. Meyers. Effective C++ (3rd Edition) (Addison-Wesley, 2005).
\[Meyers15]: S. Meyers. Effective Modern C++ (O'Reilly, 2015).
\[Murray93]: R. Murray. C++ Strategies and Tactics (Addison-Wesley, 1993).
\[Stroustrup94]: B. Stroustrup. The Design and Evolution of C++ (Addison-Wesley, 1994).
\[Stroustrup00]: B. Stroustrup. The C++ Programming Language (Special 3rdEdition) (Addison-Wesley, 2000).
\[Stroustrup05]: B. Stroustrup. A rationale for semantically enhanced library languages.
\[Stroustrup13]: B. Stroustrup. The C++ Programming Language (4th Edition). Addison-Wesley 2013.
\[Stroustrup14]: B. Stroustrup. A Tour of C++. Addison-Wesley 2014.
\[Stroustrup15]: B. Stroustrup, Herb Sutter, and G. Dos Reis: A brief introduction to C++'s model for type- and resource-safety.
\[SuttHysl04b]: H. Sutter and J. Hyslop. Collecting Shared Objects (C/C++ Users Journal, 22(8), August 2004).
\[SuttAlex05]: H. Sutter and A. Alexandrescu. C++ Coding Standards. Addison-Wesley 2005.
\[Sutter00]: H. Sutter. Exceptional C++ (Addison-Wesley, 2000).
\[Sutter02]: H. Sutter. More Exceptional C++ (Addison-Wesley, 2002).
\[Sutter04]: H. Sutter. Exceptional C++ Style (Addison-Wesley, 2004).
\[Taligent94]: Taligent's Guide to Designing Programs (Addison-Wesley, 1994).
- <a name="Abrahams01"></a>
- <a name="Alexandrescu01"></a>
- <a name="Cplusplus03"></a>
- <a name="Cargill92"></a>
- <a name="Cline99"></a>
- <a name="Dewhurst03"></a>
- <a name="Henricson97"></a>
- <a name="Koenig97"></a>
- <a name="Lakos96"></a>
- <a name="Meyers96"></a>
- <a name="Meyers97"></a>
- <a name="Meyers01"></a>
- <a name="Meyers05"></a>
- <a name="Meyers15"></a>
- <a name="Murray93"></a>
- <a name="Stroustrup94"></a>
- <a name="Stroustrup00"></a>
- <a name="Stroustrup05"></a>
- <a name="Stroustrup13"></a>
- <a name="Stroustrup14"></a>
- <a name="Stroustrup15"></a>
- <a name="SuttHysl04b"></a>
- <a name="SuttAlex05"></a>
- <a name="Sutter00"></a>
- <a name="Sutter02"></a>
- <a name="Sutter04"></a>
- <a name="Taligent94"></a>