Статьи

С ++ Кратко: C ++ Языковые употребления и идиомы

Мы обсуждали идиому RAII. Некоторые языковые употребления и идиомы программирования на C ++ могут показаться на первый взгляд чуждыми или бессмысленными, но у них есть цель. В этой главе мы рассмотрим некоторые из этих странных употреблений и идиом, чтобы понять, откуда они пришли и почему они используются.

Обычно вы увидите, что C ++ увеличивает целое число, используя синтаксис ++ i вместо i ++. Причина этого отчасти историческая, отчасти полезная, отчасти своего рода тайное рукопожатие. Одно из общих мест, которые вы увидите, это цикл for (например, for (int i = 0; i < someNumber; ++i) { ... } ). Почему программисты на C ++ используют ++ i, а не i ++? Давайте рассмотрим, что означают эти два оператора.

1
2
3
int i = 0;
   int x = ++i;
   int y = i++;

В предыдущем коде, когда все три оператора завершат выполнение, я буду равен 2. Но чем будут равны x и y? Они оба будут равны 1. Это потому, что оператор предварительного увеличения в операторе ++ i означает «увеличить i и дать в качестве результата новое значение i». Поэтому при назначении x его значения i изменяется от 0 до 1, и новое значение i, 1, присваивается х. Оператор постинкремента в выражении i ++ означает «увеличить i и дать исходное значение i в качестве результата». Поэтому при присвоении y его значения i изменяется от 1 до 2, и назначается исходное значение i, 1 к тебе

Если бы мы пошагово разложили эту последовательность инструкций, как написано, исключив операторы пре-инкремента и пост-инкремента и заменив их регулярным сложением, мы бы поняли, что для выполнения присваивания y нам нужна дополнительная переменная чтобы сохранить первоначальное значение я. Результат будет примерно таким:

01
02
03
04
05
06
07
08
09
10
int i = 0;
    
   // int x = ++i;
   i = i + 1;
   int x = i;
    
   // int y = i++;
   int magicTemp = i;
   i = i + 1;
   int y = magicTemp;

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

1
2
3
4
5
6
7
8
9
int i = 0;
    
   // int x = ++i;
   i = i + 1;
   int x = i;
    
   // int y = i++;
   int y = i;
   i = i + 1;

В некотором смысле, синтаксис ++ i (особенно внутри цикла for) является пережитком с первых дней существования C ++ и даже C до него. Зная, что другие программисты C ++ используют его, использование его самостоятельно позволяет другим узнать, что вы хотя бы немного знакомы с использованием и стилем C ++ — секретным рукопожатием. Полезной частью является то, что вы можете написать одну строку кода, int x = ++i; и получите желаемый результат вместо написания двух строк кода: i++; сопровождаемый int x = i; ,

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


В начале своей жизни C ++ принял много вещей из C, включая использование двоичного нуля в качестве представления нулевого значения. Это создало бесчисленные ошибки за эти годы. Я не обвиняю Кернигана, Ричи, Страуструпа или кого-либо еще в этом; Удивительно, как много они достигли при создании этих языков, учитывая компьютеры, доступные в 70-х и начале 80-х годов. Попытка выяснить, какие проблемы будут возникать при создании компьютерного языка, является чрезвычайно сложной задачей.

Тем не менее, на раннем этапе программисты поняли, что использование литерала 0 в их коде может привести к путанице в некоторых случаях. Например, представьте, что вы написали:

1
2
3
4
5
int* p_x = p_d;
    
   // More code here…
    
   p_x = 0;

Вы хотели установить указатель на ноль в том виде, в каком он был записан (т.е. p_x = 0;), или вы хотели установить значение указателя на 0 (т.е. * p_x = 0;)? Даже при наличии кода разумной сложности отладчику может потребоваться значительное время для диагностики таких ошибок.

Результатом этой реализации стало принятие макроса препроцессора NULL: #define NULL 0 . Это поможет уменьшить количество ошибок, если вы увидели *p_x = NULL; или p_x = 0; затем, если вы и другие программисты постоянно используете макрос NULL, ошибку будет легче обнаружить, исправить, а исправить будет легче.

Но поскольку макрос NULL является определением препроцессора, компилятор никогда не увидит ничего, кроме 0, из-за текстовой замены; он не мог предупредить вас о возможном ошибочном коде. Если кто-то переопределит макрос NULL на другое значение, могут возникнуть всевозможные дополнительные проблемы. Переопределение NULL — очень плохая вещь, но иногда программисты делают плохие вещи.

В C ++ 11 добавлено новое ключевое слово nullptr, которое может и должно использоваться вместо 0, NULL и всего остального, когда вам нужно присвоить нулевое значение указателю или проверить, является ли указатель нулевым. Есть несколько веских причин для его использования.

Ключевое слово nullptr является ключевым словом языка; это не устранено препроцессором. Поскольку он передается компилятору, он может обнаруживать ошибки и генерировать предупреждения об использовании, которые он не может обнаружить или генерировать с помощью литерала 0 или любых макросов.

Он также не может быть переопределен случайно или намеренно, в отличие от макроса, такого как NULL. Это устраняет все ошибки, которые могут возникнуть в макросах.

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

С nullptr внезапно становится возможным изменить значение null для конкретной операционной среды, не внося изменений в любой код C ++, который полностью принял nullptr. Компилятор может взять сравнение с nullptr или присваивание nullptr переменной указателя и сгенерировать любой машинный код, который требуется для целевой среды. Попытка сделать то же самое с двоичным 0 будет очень трудно, если не невозможно. Если в будущем кто-то решит спроектировать компьютерную архитектуру и операционную систему, в которой для всех адресов памяти будет добавлен бит флага нуля, чтобы обозначить ноль, современный C ++ может поддержать это из-за nullptr.


Обычно вы увидите, как люди пишут код, например if (nullptr == p_a) { ... } . Я не придерживался этого стиля в примерах, потому что он просто выглядит неправильно для меня. За 18 лет, когда я писал программы на C и C ++, у меня никогда не было проблем с тем, чего избегает этот стиль. Тем не менее, у других людей были такие проблемы. Этот стиль может быть частью правил стиля, которым вы должны следовать; следовательно, это стоит обсудить.

Если вы написали if (p_a = nullptr) { ... } вместо if (p_a == nullptr) { ... } , то ваша программа присвоит нулевое значение p_a, а оператор if всегда будет иметь значение false. C ++, благодаря своему наследию C, позволяет вам иметь выражение, которое оценивает любой целочисленный тип в круглых скобках оператора управления, например, if. C # требует, чтобы результатом любого такого выражения было логическое значение. Поскольку вы не можете присвоить значение чему-либо, например, nullptr, или постоянным значениям, таким как 3 и 0,0F, если вы поместите это значение R слева от проверки на равенство, компилятор предупредит вас об ошибке. Это потому, что вы будете присваивать значение чему-то, для чего не может быть назначено значение.

По этой причине некоторые разработчики взялись за написание проверок на равенство таким образом. Важная часть не в том, какой стиль вы выберете, а в том, что вы знаете, что присваивание внутри чего-либо, такого как выражение if, допустимо в C ++. Таким образом, вы знаете, чтобы следить за такими проблемами.

Что бы вы ни делали, не пишите намеренно такие заявления, как if (x = 3) { ... } . Это очень плохой стиль, который делает ваш код более сложным для понимания и более склонным к появлению ошибок.


Примечание. Начиная с Visual Studio 2012 RC, компилятор Visual C ++ принимает, но не реализует спецификации исключений. Однако, если вы включите спецификацию исключения throw (), компилятор, вероятно, оптимизирует любой код, который он в противном случае сгенерирует, для поддержки разматывания при возникновении исключения. Ваша программа может не работать должным образом, если исключение выдается из функции, отмеченной с помощью throw (). Другие компиляторы, которые реализуют спецификации throw, будут ожидать, что они будут помечены должным образом, поэтому вы должны реализовать правильные спецификации исключений, если ваш код должен быть скомпилирован с другим компилятором.

Примечание. Спецификации исключений с использованием синтаксиса throw () (так называемые спецификации динамических исключений) устарели в C ++ 11. Как таковые, они могут быть удалены из языка в будущем. Спецификация и оператор noexcept являются заменой для этой языковой функции, но не реализованы в Visual C ++ с Visual Studio 2012 RC.

Функции C ++ могут указывать с помощью ключевого слова спецификации исключений throw (), генерировать ли исключения и, если да, какой тип генерировать.

Например, int AddTwoNumbers(int, int) throw(); объявляет функцию, которая из-за пустых скобок заявляет, что не выдает никаких исключений, за исключением тех, которые она перехватывает внутренне и не перезапускает. Напротив, int AddTwoNumbers(int, int) throw(std::logic_error); объявляет функцию, которая заявляет, что может std::logic_error исключение типа std::logic_error или любого std::logic_error типа, полученного из этого.

Объявление функции int AddTwoNumber(int, int) throw(...); заявляет, что может выдать исключение любого типа. Этот синтаксис специфичен для Microsoft, поэтому вам следует избегать его для кода, который, возможно, должен быть скомпилирован с чем-то другим, чем компилятор Visual C ++.

Если не указан спецификатор, например, в int AddTwoNumbers(int, int); затем функция может выдать любой тип исключения. Это эквивалентно наличию спецификатора throw(...) .

В C ++ 11 добавлены новая спецификация и оператор noexcept (выражение bool). Visual C ++ не поддерживает их с Visual Studio 2012 RC, но мы кратко обсудим их, поскольку они, несомненно, будут добавлены в будущем.

Спецификатор noexcept(false) является эквивалентом как throw(...) и функции без спецификатора throw. Например, int AddTwoNumbers(int, int) noexcept(false); эквивалент обоих int AddTwoNumber(int, int) throw(...); и int AddTwoNumbers(int, int); ,

Спецификаторы noexcept(true) и noexcept эквивалентны throw() . Другими словами, все они указывают, что функция не позволяет никаким исключениям выходить из нее.

При переопределении виртуальной функции-члена спецификация исключений функции переопределения в производном классе не может указывать исключения, кроме тех, которые объявлены для типа, который она переопределяет. Давайте посмотрим на пример.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <stdexcept>
#include <exception>
 
class A
{
public:
    A(void) throw(…);
    virtual ~A(void) throw();
 
    virtual int Add(int, int) throw(std::overflow_error);
    virtual float Add(float, float) throw();
    virtual double Add(double, double) throw(int);
};
 
class B : public A
{
public:
    B(void);
    virtual ~B(void) throw();
 
    // The int Add override is fine since you can always throw less in
    // an override than the base says it can throw.
    virtual int Add(int, int) throw() override;
 
    // The float Add override here is invalid because the A version says
    // it will not throw, but this override says it can throw an
    // std::exception.
    virtual float Add(float, float) throw(std::exception) override;
 
    // The double Add override here is invalid because the A version says
    // it can throw an int, but this override says it can throw a double,
    // which the A version does not specify.
    virtual double Add(double, double) throw(double) override;
};

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

noexcept(bool expression) также является оператором. При использовании в качестве оператора оно принимает выражение, которое оценивается как истинное, если оно не может вызвать исключение, или ложное, если оно может вызвать исключение. Обратите внимание, что результатом является простая оценка; он проверяет, являются ли все вызываемые функции noexcept(true) , и есть ли в выражении операторы throw. Если он находит какие-либо операторы throw, даже те, о которых вы знаете, недоступны (например, if (x % 2 < 0) { throw "This computer is broken"; } ), он может, тем не менее, получить значение false, поскольку компилятор не требуется сделать глубокий анализ.


Идиома указателя на реализацию — это старая техника, которой уделяется много внимания в C ++. Это хорошо, потому что это очень полезно. Суть этой техники в том, что в вашем заголовочном файле вы определяете открытый интерфейс вашего класса. Единственный элемент данных, который у вас есть, — это закрытый указатель на заранее объявленный класс или структуру (обернутый в std::unique_ptr для безопасной обработки исключительной std::unique_ptr ), который будет служить фактической реализацией.

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

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

Вот простой пример.

Образец: PimplSample \ Sandwich.h

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
#pragma once
#include <memory>
 
class SandwichImpl;
 
class Sandwich
{
public:
    Sandwich(void);
    ~Sandwich(void);
 
    void AddIngredient(const wchar_t* ingredient);
    void RemoveIngredient(const wchar_t* ingredient);
    void SetBreadType(const wchar_t* breadType);
    const wchar_t* GetSandwich(void);
 
private:
    std::unique_ptr<SandwichImpl> m_pImpl;
};

Образец: PimplSample \ Sandwich.cpp

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
#include «Sandwich.h»
#include <vector>
#include <string>
#include <algorithm>
 
using namespace std;
 
// We can make any changes we want to the implementation class without
// triggering a recompile of other source files that include Sandwich.h since
// SandwichImpl is only defined in this source file.
// file needs to be recompiled if we make changes to SandwichImpl.
class SandwichImpl
{
public:
    SandwichImpl();
    ~SandwichImpl();
 
    void AddIngredient(const wchar_t* ingredient);
    void RemoveIngredient(const wchar_t* ingredient);
    void SetBreadType(const wchar_t* breadType);
 
    const wchar_t* GetSandwich(void);
 
private:
    vector<wstring> m_ingredients;
    wstring m_breadType;
    wstring m_description;
};
 
SandwichImpl::SandwichImpl()
{
}
 
SandwichImpl::~SandwichImpl()
{
}
 
void SandwichImpl::AddIngredient(const wchar_t* ingredient)
{
    m_ingredients.emplace_back(ingredient);
}
 
void SandwichImpl::RemoveIngredient(const wchar_t* ingredient)
{
    auto it = find_if(m_ingredients.begin(), m_ingredients.end(), [=] (wstring item) -> bool
    {
        return (item.compare(ingredient) == 0);
    });
 
    if (it != m_ingredients.end())
    {
        m_ingredients.erase(it);
    }
}
 
void SandwichImpl::SetBreadType(const wchar_t* breadType)
{
    m_breadType = breadType;
}
 
const wchar_t* SandwichImpl::GetSandwich(void)
{
    m_description.clear();
    m_description.append(L»A «);
    for (auto ingredient : m_ingredients)
    {
        m_description.append(ingredient);
        m_description.append(L», «);
    }
    m_description.erase(m_description.end() — 2, m_description.end());
    m_description.append(L» on «);
    m_description.append(m_breadType);
    m_description.append(L».»);
 
    return m_description.c_str();
}
 
Sandwich::Sandwich(void)
    : m_pImpl(new SandwichImpl())
{
}
 
Sandwich::~Sandwich(void)
{
}
 
void Sandwich::AddIngredient(const wchar_t* ingredient)
{
    m_pImpl->AddIngredient(ingredient);
}
 
void Sandwich::RemoveIngredient(const wchar_t* ingredient)
{
    m_pImpl->RemoveIngredient(ingredient);
}
 
void Sandwich::SetBreadType(const wchar_t* breadType)
{
    m_pImpl->SetBreadType(breadType);
}
 
const wchar_t* Sandwich::GetSandwich(void)
{
    return m_pImpl->GetSandwich();
}

Образец: PimplSample \ PimplSample.cpp

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <ostream>
 
#include «Sandwich.h»
#include «../pchar.h»
 
using namespace std;
 
int _pmain(int /*argc*/, _pchar* /*argv*/[])
{
    Sandwich s;
    s.AddIngredient(L»Turkey»);
    s.AddIngredient(L»Cheddar»);
    s.AddIngredient(L»Lettuce»);
    s.AddIngredient(L»Tomato»);
    s.AddIngredient(L»Mayo»);
    s.RemoveIngredient(L»Cheddar»);
    s.SetBreadType(L»a Roll»);
 
    wcout << s.GetSandwich() << endl;
 
    return 0;
}

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

Этот урок представляет собой главу из C ++ Succinctly , бесплатной книги от команды Syncfusion .