Какие ваши любимые идиомы в стиле C++
Какие ваши любимые идиомы в стиле C++? Я спрашиваю о стиле или типе кодирования, например, о том, где вы помещаете фигурные скобки, есть ли пробелы после ключевых слов, размер отступов и т. Д. Это противоречит рекомендациям или требованиям, таким как удаление массивов с помощью delete[]
,
Вот пример одного из моих любимых: В инициализаторах класса C++ мы помещаем разделители в начале строки, а не в конце. Это облегчает держать это в курсе. Это также означает, что различия между версиями исходного кода более чистые.
TextFileProcessor::
TextFileProcessor( class ConstStringFinder& theConstStringFinder )
: TextFileProcessor_Base( theConstStringFinder )
, m_ThreadHandle ( NULL )
, m_startNLSearch ( 0 )
, m_endNLSearch ( 0 )
, m_LineEndGetIdx ( 0 )
, m_LineEndPutIdx ( 0 )
, m_LineEnds ( new const void*[ sc_LineEndSize ] )
{
;
}
24 ответа
При создании перечислений поместите их в пространство имен, чтобы вы могли получить к ним доступ с осмысленным именем:
namespace EntityType {
enum Enum {
Ground = 0,
Human,
Aerial,
Total
};
}
void foo(EntityType::Enum entityType)
{
if (entityType == EntityType::Ground) {
/*code*/
}
}
РЕДАКТИРОВАТЬ: Тем не менее, эта техника устарела в C++11. Перечисление с областью действия (объявляется с enum class
или же enum struct
) следует использовать вместо этого: он более безопасен для типов, лаконичен и гибок. При перечислениях в старом стиле значения помещаются во внешнюю область. При перечислении нового стиля они помещаются в рамки enum class
название.
Предыдущий пример переписан с использованием перечисления с областью видимости (также известного как строго типизированное перечисление):
enum class EntityType {
Ground = 0,
Human,
Aerial,
Total
};
void foo(EntityType entityType)
{
if (entityType == EntityType::Ground) {
/*code*/
}
}
Существуют и другие существенные преимущества использования перечислений с областью видимости: отсутствие неявного приведения, возможное прямое объявление и возможность использовать пользовательский базовый тип (не по умолчанию). int
).
RAII: приобретение ресурсов - это инициализация
RAII может быть самой важной идиомой. Идея состоит в том, что ресурсы должны быть сопоставлены с объектами, чтобы их время жизни управлялось автоматически в соответствии с областью, в которой эти объекты объявлены.
Например, если дескриптор файла был объявлен в стеке, он должен быть неявно закрыт после того, как мы вернемся из функции (или цикла, или того объема, в котором он был объявлен). Если динамическое распределение памяти было выделено как член класса, оно должно быть неявно освобождено, когда этот экземпляр класса уничтожен. И так далее. Каждый вид ресурсов - выделения памяти, файловые дескрипторы, соединения с базой данных, сокеты и любые другие виды ресурсов, которые должны быть получены и освобождены - должен быть заключен в такой класс RAII, время жизни которого определяется областью, в которой он был объявлен.
Одним из основных преимуществ этого является то, что C++ гарантирует, что деструкторы вызываются, когда объект выходит из области видимости, независимо от того, как управление покидает эту область. Даже если возникнет исключение, все локальные объекты выйдут из области видимости, и связанные с ними ресурсы будут очищены.
void foo() {
std::fstream file("bar.txt"); // open a file "bar.txt"
if (rand() % 2) {
// if this exception is thrown, we leave the function, and so
// file's destructor is called, which closes the file handle.
throw std::exception();
}
// if the exception is not called, we leave the function normally, and so
// again, file's destructor is called, which closes the file handle.
}
Независимо от того, как мы покидаем функцию и что происходит после открытия файла, нам не нужно явно закрывать файл или обрабатывать исключения (например, try-finally) внутри этой функции. Вместо этого файл очищается, потому что он привязан к локальному объекту, который уничтожается, когда выходит из области видимости.
RAII также менее известен как SBRM (Scope-Bound Resource Management).
Смотрите также:
- ScopeGuard позволяет коду "автоматически вызывать операцию" отменить "... в случае возникновения исключения".
Прописи своп
Идиома копирования-обмена обеспечивает исключительное копирование. Это требует правильного копирования ctor и swap.
struct String {
String(String const& other);
String& operator=(String copy) { // passed by value
copy.swap(*this); // nothrow swap
return *this; // old resources now in copy, released in its dtor
}
void swap(String& other) throw() {
using std::swap; // enable ADL, defaulting to std::swap
swap(data_members, other.data_members);
}
private:
Various data_members;
};
void swap(String& a, String& b) { // provide non-member for ADL
a.swap(b);
}
Вы также можете напрямую реализовать метод подкачки с помощью ADL (Argument Dependent Lookup).
Эта идиома важна, потому что она обрабатывает самостоятельное назначение[1], обеспечивает гарантию сильных исключений[2] и часто очень проста в написании.
[1] Несмотря на то, что самостоятельное назначение не выполняется настолько эффективно, насколько это возможно, оно должно быть редким, поэтому, если оно никогда не происходит, это на самом деле быстрее.
[2] Если выдается какое-либо исключение, состояние объекта (*this
) не изменяется.
CRTP: любопытно повторяющийся шаблон
CRTP происходит, когда вы передаете класс в качестве параметра шаблона в его базовый класс:
template<class Derived>
struct BaseCRTP {};
struct Example : BaseCRTP<Example> {};
Внутри базового класса он может получить производный экземпляр, дополненный производным типом, просто путем приведения (либо static_cast, либо dynamic_cast work):
template<class Derived>
struct BaseCRTP {
void call_foo() {
Derived& self = *static_cast<Derived*>(this);
self.foo();
}
};
struct Example : BaseCRTP<Example> {
void foo() { cout << "foo()\n"; }
};
По сути, call_foo был внедрен в производный класс с полным доступом к членам производного класса.
Не стесняйтесь редактировать и добавлять конкретные примеры использования, возможно, к другим сообщениям SO.
pImpl: указатель на реализацию
Идиома pImpl - очень полезный способ отделить интерфейс класса от его реализации.
Обычно определение класса должно содержать переменные-члены, а также методы, которые могут предоставлять слишком много информации. Например, переменная-член может иметь тип, определенный в заголовке, который мы не хотим включать везде.
windows.h
заголовок является ярким примером здесь. Мы можем пожелать обернуть HANDLE
или другой тип Win32 внутри класса, но мы не можем поставить HANDLE
в определении класса без необходимости включать windows.h
везде класс используется.
Тогда решение состоит в том, чтобы создать частную реализацию IMPL или P- ointer- to- IMPL класса, и позволить общедоступной реализации хранить только указатель на частную, и пересылать все методы-члены.
Например:
class private_foo; // a forward declaration a pointer may be used
// foo.h
class foo {
public:
foo();
~foo();
void bar();
private:
private_foo* pImpl;
};
// foo.cpp
#include whichever header defines the types T and U
// define the private implementation class
class private_foo {
public:
void bar() { /*...*/ }
private:
T member1;
U member2;
};
// fill in the public interface function definitions:
foo::foo() : pImpl(new private_foo()) {}
foo::~foo() { delete pImpl; }
void foo::bar() { pImpl->bar(); }
Реализация foo
теперь отсоединен от своего открытого интерфейса, так что
- он может использовать члены и типы из других заголовков, не требуя присутствия этих зависимостей при использовании класса, и
- реализация может быть изменена без принудительной перекомпиляции кода, который использует класс.
Пользователи класса просто включают заголовок, который не содержит ничего конкретного о реализации класса. Все детали реализации содержатся внутри foo.cpp
,
Мне нравится выстраивать код / инициализации в "столбцах"... Оказывается очень полезным при редактировании с помощью редактора, поддерживающего режим "столбец", и, кажется, мне намного легче читать...
int myVar = 1; // comment 1
int myLongerVar = 200; // comment 2
MyStruct arrayOfMyStruct[] =
{
// Name, timeout, valid
{"A string", 1000, true }, // Comment 1
{"Another string", 2000, false }, // Comment 2
{"Yet another string", 11111000, false }, // Comment 3
{NULL, 5, true }, // Comment 4
};
Напротив, появится тот же код без отступа и отформатированный, как указано выше... (немного сложнее для чтения на моих глазах)
int myVar = 1; // comment 1
int myLongerVar = 200; // comment 2
MyStruct arrayOfMyStruct[] =
{
// Name, timeout, valid
{"A string", 1000, true},// Comment 1
{"Another string", 2000, false }, // Comment 2
{"Yet another string", 11111000,false}, // Comment 3
{NULL, 5, true }, // Comment 4
};
Публичный топ - Приватный даун
Казалось бы, небольшая оптимизация, но с тех пор, как я перешел на это соглашение, у меня стало намного больше времени для занятий, особенно после того, как я не смотрел на них в течение 42 лет.
Наличие постоянной видимости членов, переходя от пунктов, представляющих частый интерес, к скучным вещам, чрезвычайно полезно, особенно когда код должен самодокументироваться.
(sidenote для пользователей qt: слоты идут перед сигналами, потому что они должны вызываться как функции-члены, не относящиеся к слотам, и помимо их слотов должны быть неотличимы от не-слотов)
- Общественный, охраняемый, частный
- затем Factory, ctor, dtor, копирование, обмен
- затем класс 'Интерфейс В самом конце, в отдельном
private:
раздел, идет данные (в идеале только Impl-указатель).
Это правило также помогает, если у вас есть проблемы с сохранением объявления вашего класса в беспорядке.
class Widget : public Purple {
public:
// Factory methods.
Widget FromRadians (float);
Widget FromDegrees (float);
// Ctors, rule of three, swap
Widget();
Widget (Widget const&);
Widget &operator = (Widget const &);
void swap (Widget &) throw();
// Member methods.
float area() const;
// in case of qt {{
public slots:
void invalidateBlackHole();
signals:
void areaChanged (float);
// }}
protected:
// same as public, but for protected members
private:
// same as public, but for private members
private:
// data
float widgetness_;
bool isMale_;
};
В if
В заявлениях, когда возникают сложные условия, вы можете четко показать, на каком уровне каждое условие использует отступ.
if ( ( (var1A == var2A)
|| (var1B == var2B))
&& ( (var1C == var2C)
|| (var1D == var2D)))
{
// do something
}
re: ididak
Я исправляю код, который разбивает длинные операторы на слишком много коротких строк.
Посмотрим правде в глаза: это уже не 90-е годы. Если ваша компания не может позволить себе широкоформатные ЖК-дисплеи для своих кодеров, вам нужно получить лучшую работу:)
Полиморфизм во время компиляции
(Также известный как синтаксический полиморфизм и статический полиморфизм, в отличие от полиморфизма во время выполнения.)
С помощью шаблонных функций можно написать код, основанный на конструкторах типов и сигнатурах вызовов семейств параметризованных типов, без необходимости вводить общий базовый класс.
В книге " Элементы программирования" авторы называют эту трактовку типов абстрактным родом. С концепциями можно определить требования к таким параметрам типа, хотя C++ не требует таких спецификаций.
Два простых примера:
#include <stdexcept>
template <typename T>
T twice(T n) {
return 2 * n;
}
InIt find(InIt f, InIt l,
typename std::iterator_traits<InIt>::reference v)
{
while (f != l && *f != v)
++f;
return f;
}
int main(int argc, char* argv[]) {
if (6 != twice(3))
throw std::logic_error("3 x 2 = 6");
int const nums[] = { 1, 2, 3 };
if (nums + 4 != find(nums, nums + 4, 42))
throw std::logic_error("42 should not have been found.");
return 0;
}
Можно позвонить twice
с любым обычным типом, который имеет двоичный *
оператор определен. Точно так же можно позвонить find()
с любыми типами, которые сопоставимы и что модель Input Iterator. Один набор кода работает одинаково для разных типов, без общих базовых классов.
Конечно, здесь на самом деле происходит то, что это тот же самый исходный код, расширяемый в различные специфические для типа функции во время создания шаблона, каждая из которых имеет отдельный сгенерированный машинный код. Приспособление одного и того же набора типов без шаблонов потребовало бы либо 1) отдельных рукописных функций с конкретными сигнатурами, либо 2) полиморфизма во время выполнения через виртуальные функции.
Нет избранного, но я исправлю код, который имеет:
- tabs - вызывает смещение во многих IDE и инструментах просмотра кода, потому что они не всегда согласуются с табуляцией в моде 8 пробелов.
- строки длиннее 80 столбцов - давайте посмотрим правде в глаза, более короткие строки более читабельны. Мой мозг может анализировать большинство соглашений о кодировании, если строки короткие.
- строки с конечными пробелами - git будет жаловаться на это как на пробельные ошибки, которые отображаются в виде красных пятен в diff, что раздражает.
Вот одна строка, чтобы найти оскорбительные файлы:
git grep -I -E '<tab>|.{81,}| *$' | cut -f1 -d: | sort -u
где <tab>
является символом табуляции (регулярное выражение POSIX не делает \t)
if/while/ для заключенных в скобки выражений с разделителем пробела
if (expression) // preferred - if keyword sticks out more
против
if(expression) // looks too much like a void function call
Я предполагаю, что это подразумевает, что мне нравятся мои вызовы функций, чтобы НЕ иметь разделитель пробела
foo(parm1, parm2);
Шаблон и крючок
Это способ обработки как можно большего количества элементов в каркасе и предоставления пользователям или пользователям возможности настройки или настройки. Также известный как Hotspot и Template Method.
class Class {
void PrintInvoice(); // Called Template (boilerplate) which uses CalcRate()
virtual void CalcRate() = 0; // Called Hook
}
class SubClass : public Class {
virtual void CalcRate(); // Customized method
}
Описано Вольфгангом При в своей книге " Шаблоны проектирования для объектно-ориентированной разработки программного обеспечения".
После работы с кем-то, кто был частично слепым - и по его просьбе - я переключился на использование намного большего количества пробелов. В то время мне это не нравилось, но теперь я предпочитаю. Вдобавок ко всему, единственное место, где нет пробелов между идентификаторами и ключевыми словами, и тому подобное - после имени функции и перед следующими круглыми скобками.
void foo( int a, int b )
{
int c = a + ( a * ( a * b ) );
if ( c > 12 )
c += 9;
return foo( 2, c );
}
Мне действительно нравится помещать небольшое утверждение в ту же строку, как если бы
int myFunc(int x) {
if(x >20) return -1;
//do other stuff ....
}
Не уверен, что это считается идиомой, но я склонен использовать встроенные комментарии в стиле doxygen, даже если в проекте не используется doxygen...
bool MyObjects::isUpToSomething() ///< Is my object up to something
(в сторону. Мои комментарии обычно не такие отстойные.)
Я не знаю, подходит ли он как идиома, но довольно большая часть тяжелого программирования шаблонов зависит (часто сильно) от SFINAE (ошибка замещения не является ошибкой). У пары ответов на предыдущий вопрос есть примеры.
Документируйте возвращаемые значения в строке функций, чтобы их было легко найти.
int function(void) /* return 1 on success, 0 on failure */
{
return 1;
};
Полезно помещать имена функций в новую строку, чтобы вы могли
grep -R '^fun_name' .
для них. Я видел этот стиль, используемый для множества проектов GNU, и мне нравится:
static void
fun_name (int a, int b) {
/* ... */
}
Я бы предложил PIMPL или, как Джеймс Коплиен изначально называл это "Handle Body".
Эта идиома позволяет полностью отделить интерфейс от реализации. При работе над переписыванием и повторным выпуском основного компонента промежуточного программного обеспечения CORBA, эта идиома использовалась, чтобы полностью отделить API от реализации.
Это практически исключило любую возможность реверс-инжиниринга.
Отличным ресурсом по идиомам C++ является отличная книга Джеймса Коплиена " Расширенные стили и идиомы программирования C++". Настоятельно рекомендуется!
Редактировать: как указано ниже Нилом, эта книга довольно устарела, так как многие из его рекомендаций фактически включены в сам стандарт C++. Тем не менее, я все еще нахожу это источником полезной информации, особенно. в форме его PLoP-статьи по идиомам C++, где многие идиомы были преобразованы в более позднюю форму.
Запишите каждый аргумент метода или функции в отдельной строке, чтобы их можно было легко комментировать.
int ReturnMaxValue(
int* inputList, /* the list of integer values from which to get the maximum */
long size, /* count of the number of integer values in inputList */
char* extraArgs /* additional arguments that a caller can provide. */
)
Я всегда придираюсь и редактирую следующее:
- Лишние переводы строки
- Нет новой строки в EOF
Я склонен ставить еще один на все мои если.
if (condition)
{
complicated code goes here
}
else
{
/* This is a comment as to why the else path isn't significant */
}
Хотя это раздражает моих коллег. Вы можете сразу сказать, что я рассмотрел другой случай во время кодирования.