Pull to refresh
987.85
OTUS
Цифровые навыки от ведущих экспертов

Основные принципы C++: Правила выражений и операторов

Reading time 5 min
Views 19K
Original author: Rainer Grimm
Бобра!

Что ж, мы плавно выходим на старт второго потока группы «Разработчик С++» и разбираем интересные материалы, которые накопились у преподавателя в свободное от работы и преподавания время. Сегодня рассмотрим (а потом и продолжим) серию материалов, где разбираются отдельные пункты С++ Core Guidelines.

Поехали.

В C++ Core Guidelines много правил, посвященных выражениям и операторам. Если быть точным, то более 50 правил посвящено объявлениям, выражениям, операторам и арифметическим выражениям.



*перевод
Информативные названия

Оптимальная длина переменных

  • Не должны быть слишком длинными (maximimNumberOfPointsInModernOlympics.) или слишком короткими (например, x, x1)
  • Длинные названия сложно печатать, короткие названия недостаточно информативны..
  • Дебажить программы с названиями от 8 до 20 символов гораздо проще
  • Гайдлайны не заставляют вас срочно менять названия переменных на имена из 9-15 или 10-16 символов. Но если вы найдете в своем коде более короткие названия, убедитесь, что они достаточно информативны.

Слишком длинные: numberOfPeopleOnTheUsOlympicTeam; numberOfSeatsInTheStadium; maximumNumberOfPointsInModernOlympics
Слишком короткие: n; np; ntmn; ns; nslsd; m; mp; max; points
В самый раз: numTeamMembers, teamMembersCount



Существует два правила, являющихся общими:

Правило 1: Отдайте предпочтение стандартным библиотекам перед прочими библиотеками и “самописным” кодом

Нет смысла писать сырой цикл для суммирования вектора чисел:

int max = v.size();             // плохо: пространно, цель не ясна
double sum = 0.0;
for (int i = 0; i < max; ++i)
    sum = sum + v[i];

Просто используйте алгоритм std::accumulate из STL.

auto sum = std::accumulate(begin(a), end(a), 0.0);   // хорошо

Это правило напомнило мне слова Шона Парент (Sean Parent) с CppCon 2013: “Если вы хотите улучшить качество кода в организации, замените все принципы кодинга одной целью: никаких сырых циклов!”.

Дословно: если вы пишете сырой цикл, скорее всего вы просто не знаете алгоритмов STL.

Правило 2: Отдайте предпочтение подходящим абстракциям перед непосредственным использованием языковых особенностей

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

Понять функционал мешали неправильные абстракции, на которых он был построен.
К примеру, посмотрим на самодельную функцию для чтения std::istream:

char** read1(istream& is, int maxelem, int maxstring, int* nread)   // плохо: пространно и разрозненно
{
    auto res = new char*[maxelem];
    int elemcount = 0;
    while (is && elemcount < maxelem) {
        auto s = new char[maxstring];
        is.read(s, maxstring);
        res[elemcount++] = s;
    }
    *nread = elemcount;
    return res;
}

И, в сравнении, насколько проще воспринимается следующая функция:

vector<string> read2(istream& is)   // хорошо
{
    vector<string> res;
    for (string s; is >> s;)
        res.push_back(s);
    return res;
}

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

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

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

Вот первые шесть правил.
(нумерация идёт как в статье. Автор пропустил пункты 3 и 4 т.к. они не соответствуют тематике)

Правило 5: Придерживайтесь небольшой области видимости

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

Правило 6: Объявляйте имена в инициализаторах и условиях for-оператора, чтобы ограничить область видимости

Мы могли объявлять переменную в операторе for еще со времен первого С++ стандарта. А в C++17 мы можем объявлять переменные и в операторах if и switch.

std::map<int,std::string> myMap;

if (auto result = myMap.insert(value); result.second){  // (1)
    useResult(result.first);  
    // ...
} 
else{
    // ...
} // результат автоматически уничтожается                 // (2)

Переменная result (1) действительна только внутри веток if и else оператора if. Поэтому result не будет засорять внешнюю область видимости и автоматически уничтожится (2). Такая особенность есть только в C++17, раньше result нужно было бы объявить во внешней области видимости (3).

std::map<int,std::string> myMap;
auto result = myMap.insert(value)   // (3)
if (result.second){  
    useResult(result.first);  
    // ...
} 
else{
    // ...
}

Правило 7: Общие и локальные имена должны быть короче, чем редкие и нелокальные

Правило может показаться странным, но мы уже привыкли. Присваивая переменным имена i, j и Т, мы сразу даем понять, что i и j — это индексы, а T — тип параметра шаблона.

template<typename T>    // good
void print(ostream& os, const vector<T>& v)
{
    for (int i = 0; i < v.size(); ++i)
        os << v[i] << '\n';
}

Для этого правила есть мета-правило. Имя должно было очевидным. Если контекст небольшой, можно быстро понять, что делает переменная. Но в длинном контексте понять сложнее, поэтому нужно использовать названия длиннее.

Правило 8: Избегайте похожих имен

А вам удастся прочитать этот пример без замешательства?

if (readable(i1 + l1 + ol + o1 + o0 + ol + o1 + I0 + l0)) surprise();

Если честно, я часто с трудом различаю цифру 0 и заглавную букву O. Из-за шрифта они могут выглядеть почти одинаково. Два года назад мне потребовалось очень много времени, чтобы залогиниться на сервер. Просто потому что автоматически сгенерированный пароль содержал символ O.

Правило 9: Не используйте имена, написанные ПОЛНОСТЬЮ_КАПСОМ

Если вы пишете названия ПОЛНОСТЬЮ_КАПСОМ, то будьте готовы столкнуться с заменой на макросы — именно в них он часто используется. В части программы, представленной ниже, есть небольшой сюрприз:

// где-нибудь в заголовке:
#define NE !=

// где-нибудь в другом заголовке:
enum Coord { N, NE, NW, S, SE, SW, E, W };

// где-нибудь еще в .cpp грустного программиста:
switch (direction) {
case N:
    // ...
case NE:
    // ...
// ...
}

Правило 10: Объявляйте (только) одно имя за раз

Приведу два примера. Заметили обе проблемы?

char* p, p2;
char a = 'a';
p = &a;
p2 = a;                              // (1)

int a = 7, b = 9, c, d = 10, e = 3;  // (2)

p2 — просто char (1) и c не инициализирован (2).

В C++17 для нас есть одно исключение из этого правила: структурированное связывание.
Теперь я могу сделать if выражение с инициализатором из правила 6 еще более удобным для чтения.

std::map<int,std::string> myMap;

if (auto [iter, succeeded] = myMap.insert(value); succedded){  // (1)
    useResult(iter);  
    // ...
} 
else{
    // ...
} // iter и succeeded автоматически уничтожаются           // (2)

THE END (to be continued)

Если есть вопросы, замечания, то ждём их тут или у нас на Дне открытых дверей.
Tags:
Hubs:
+12
Comments 32
Comments Comments 32

Articles

Information

Website
otus.ru
Registered
Founded
Employees
101–200 employees
Location
Россия
Representative
OTUS