Бьерн Страуструп отвечает на топ-5 вопросов по C++ со Stack Overflow

Original author: Sonny Li
  • Translation
В преддверии старта курса «Разработчик С++» подготовили перевод интересного материала.





Мариэль Фрэнк и Сонни Ли, авторы курса Learn C++ на Codecademy, недавно получили возможность взять интервью у доктора Бьерна Страуструпа, создателя C++.

В рамках этого интервью он ответил на вопросы по C++, набравшие наибольшее количество голосов на Stack Overflow Хотя все интервью достойно полного прочтения, Codecademy великодушно разрешили нам поделиться его частью.

Если вы когда-нибудь задумывались, существуют ли на Stack Overflow определяюще исчерпывающие ответы, то вот нечто ближайшее к этому, что вы можете получить наверняка (хотя мы ожидаем, что кто-то может не согласиться).

Почему обработка отсортированного массива быстрее, чем обработка несортированного массива?



Примечание: Этот вопрос является номером 1, получившим наибольшее количество голосов на Stack Overflow за все время.


Звучит как вопрос с собеседования. Это правда? Как вы это узнали? Плохо отвечать на вопросы об эффективности, не проводя предварительных измерений, поэтому важно знать, как их измерять.
Итак, я проверял на векторе из миллиона целых чисел и получил:

Сортированный    32995 миллисекунд
Перемешанный          125944 миллисекунд

Сортированный     18610 миллисекунд
Перемешанный          133304 миллисекунд

Сортированный     17942 миллисекунд
Перемешанный          107858 миллисекунд


Я запускал это несколько раз для уверенности. Да, это явление реально. Мой основной код был:

void run(vector<int>& v, const string& label)
{
    auto t0 = system_clock::now();
    sort(v.begin(), v.end());
    auto t1 = system_clock::now();
    cout << label 
         << duration_cast<microseconds>(t1 — t0).count() 
         << " milliseconds\n";
}

void tst()
{
    vector<int> v(1'000'000);
    iota(v.begin(), v.end(), 0);
    run(v, "already sorted ");
    std::shuffle(v.begin(), v.end(), std::mt19937{ std::random_device{}() });
    run(v, "shuffled    ");
}


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

Одной из причин является предсказание ветвлений: ключевая операция в алгоритме сортировки — “if(v[i] < pivot]) …” или эквивалент. Для отсортированной последовательности этот тест всегда верен, тогда как для случайной последовательности выбранная ветвь изменяется случайным образом.

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

Quicksort (и сортировка в целом) — сложное исследование, которое привлекло некоторые из величайших умов информатики. Хорошая функция сортировки — это результат выбора хорошего алгоритма и внимания к производительности оборудования при его реализации.
Если вы хотите написать эффективный код, вам нужно принимать во внимание архитектуру машины.
———————————————————————————————
Простите за вмешательство. Просто напоминаем, что подкаст Stack Overflow возвращается! Заглядывайте к нам и услышите интервью с нашим новым генеральным директором.

Что такое оператор --> в C++?




Это старый вопрос с подвохом. В С ++ нет оператора -->. Рассмотрите следующее:

if (p-->m == 0) f(p);


Это, конечно, выглядит так, как будто существует некий оператор --> и с подходящим объявлением p и m, вы даже сможете скомпилировать и запустить это:

int p = 2;
int m = 0;
if (p-->m == 0) f(p);


Это фактически означает: посмотрите, больше ли p--, чем m (так оно и есть), а затем сравните результат (true) с 0. Что ж, true != 0, поэтому результат равен false и f() не вызывается. Другими словами:

if ((p--) > m == 0) f(p);


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

Лучшее руководство и список книг C++



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

Я провел небольшое исследование в Интернете и нашел удивительный набор рекомендаций. Многие были серьезно устаревшими, а некоторые были просто плохими. Новичок, ищущий хорошую книгу без руководства, будет очень смущен!

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

Я рекомендую свое Программирование: Принципы и Практика использования C++ (2-е издание) для людей, только начинающих учиться программировать, и Тур по C++ (2-е издание) для людей, которые уже являются программистами и которым необходимо ознакомиться с современным C++. Люди с сильным математическим образованием могут начать с Открытия современного C++: интенсивный курс для ученых, инженеров и программистов Питера Готшлинга.

Как только вы начнете использовать C++ по-настоящему, вам потребуется набор руководящих принципов, чтобы различать, что можно сделать, и что является хорошей практикой. Для этого я рекомендую C++ Core Guidelines на GitHub.

Для хороших кратких объяснений отдельных языковых особенностей и функций стандартной библиотеки я рекомендую www.cppreference.com.

#4. Каковы различия между переменной-указателем и ссылочной переменной в C++?



Чтобы узнать больше о ссылках и указателях, ознакомьтесь с Learn C++.

Обе представлены в памяти как машинный адрес. Разница в их использовании.
Чтобы инициализировать указатель, вы даете ему адрес объекта:

int x = 7;
int* p1 = &x;
int* p2 = new int{9};


Для чтения и записи через указатель мы используем оператор разыменования (*):

*p1 = 9;       // write through p1
int y = *p2;   // read through p2


Когда мы присваиваем один указатель другому, они оба будут указывать на один и тот же объект:

p1 = p2;       // теперь p1 и p2 оба указывают на int со значением 9
*p2 = 99;      // записываем 99 через p2
int z = *p1;   // считываем через p1, z становится 99 (а не 9)


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

Для ссылок разыменование является неявным. Вы инициализируете ссылку с объектом, и ссылка получает его адрес.

int x = 7;
int& r1 = x;
int& r2 = *new int{9};


Оператор new возвращает указатель, поэтому мне пришлось разыменовать его перед назначением, используя его для инициализации ссылки.

Для чтения и записи через ссылку мы просто используем имя ссылки (без явной разыменования):

r1 = 9;        // записываем через r1
int y = r2;    // считываем через r2


Когда мы присваиваем одну ссылку другой, указанное значение будет скопировано:

r1 = r2;       // теперь p1 и p2 обе имеют одинаковое значение 9
r1 = 99;       // записываем 99 через r1
int z = r2;    // считываем через r2, z становится 9 (а не 99)


И ссылки, и указатели часто используются в качестве аргументов функции:

void f(int* p)

{
    if (p == nullptr) return;
    // ...
}

void g(int& r)
{
    // ...
}

int x = 7;
f(&x);
g(x);


Указатель может быть nullptr, поэтому мы должны проверять, указывает ли он на что-либо. Про ссылку можно изначально предположить, что она ссылается на что-то.

#5. Как итерировать по словам строки?




Используйте stringstream, но как вы определяете «слово»? Подумайте: «У Мэри был маленький ягненок.». Последнее слово «ягненок» или «ягненок.»? Если пунктуации нет, это легко:

vector<string> split(const string& s)
{
    stringstream ss(s);
    vector<string> words;
    for (string w; ss>>w; ) words.push_back(w);
    return words;
}

auto words = split("here is a simple example");   // five words
for (auto& w : words) cout << w << '\n';


или просто:

for (auto& w : split("here is a simple example")) cout << w << '\n';


По умолчанию оператор >> пропускает пробелы. Если нам нужны произвольные наборы разделителей, все становится немного более запутанным:

template<typename Delim>
string get_word(istream& ss, Delim d)
{
    string word;
    for (char ch; ss.get(ch); )    // пропуск разделителей
        if (!d(ch)) {
            word.push_back(ch);
            break;
        }
    for (char ch; ss.get(ch); )    // сбор слова
        if (!d(ch))
            word.push_back(ch);
        else
            break;
    return word;
}


d — это операция, сообщающая, является ли символ разделителем, и я возвращаю "" (пустую строку), чтобы указать, что не было слова для возврата.

vector<string> split(const string& s, const string& delim)
{
    stringstream ss(s);
    auto del = [&](char ch) { for (auto x : delim) if (x == ch) return true; return false; };

    vector<string> words;
    for (string w; (w = get_word(ss, del))!= ""; ) words.push_back(w);
    return words;
}

auto words = split("Now! Here is something different; or is it? ", "!.,;? ");
for (auto& w : words) cout << w << '\n';


Если у вас есть библиотека C++20 Range, вам не нужно писать что-то подобное, но вы можете использовать split_view.

Бьярн Страуструп — технический партнер и управляющий директор Morgan Stanley в Нью-Йорке и приглашенный профессор Колумбийского Университета. Он также является создателем C++.

Для получения дополнительной информации о C++20 смотрите: isocpp.org.


На этом все. До встречи на курсе!
OTUS. Онлайн-образование
Цифровые навыки от ведущих экспертов

Comments 7

    0
    3 абзаца и 3 предложения? Это самое короткое интервью и статья на хабре, которую я когда-либо видел)
      0
      Уже поправили. Верстка поехала))
        +1
        Спасибо. Почитаем)
      +2
      ужасное интервью. понятно, что проблема не ваша, оно такое в оригинале, но зачем такое переводить?

      * первый вопрос, почему цикл for(...) if (data[c] > 128) sum+=data[c]; работает в шесть раз быстрее на отсортированном массиве (правильный ответ: из-за предсказания ветвлений). Ответ — почему sort() быстрее работает на отсортированном массиве. Дык, это ж другой вопрос. Очевидно же, что сортировка отсортированного может быть быстрой. Пузырёк, например, может за O(N) работать на отсортированом массиве, и что?

      * Второй вопрос, про оператор «изменяется по направлению к», while (x --> 0) {...}. Ответ — рассмотрим if (p-->m == 0) f(p);. Но это ж не «изменяется по направлению к».

      * Пятый вопрос «как мне пройтись итератором по всем словам в строке, если 'слово' это последовательность символов, разделённая пробелами». Ответ — «stringstream, но как вы определяете слово?»

      Видно, что вопросы явно дальше заголовка не читались.
        –2

        У нашей Мэри был баран.

          0
          Добавлю про оператор "-->". Его можно использовать в цикле while следующим образом:
          size_t i = 10;
          while (i --> 0) {
              std::cout << i << std::endl;
          }
          

          Получается идиома «делать, пока i стремится к нулю».
            0
            Вот только «пока i стремится к нулю» не должно учитывать ноль. А это конструкция будет работать и при i==0

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