В C ++ я плачу за то, что не ем?


170

Давайте рассмотрим следующие примеры hello world на C и C ++:

main.c

#include <stdio.h>

int main()
{
    printf("Hello world\n");
    return 0;
}

main.cpp

#include <iostream>

int main()
{
    std::cout<<"Hello world"<<std::endl;
    return 0;
}

Когда я компилирую их в godbolt для сборки, размер кода C составляет всего 9 строк ( gcc -O3):

.LC0:
        .string "Hello world"
main:
        sub     rsp, 8
        mov     edi, OFFSET FLAT:.LC0
        call    puts
        xor     eax, eax
        add     rsp, 8
        ret

Но размер кода C ++ составляет 22 строки ( g++ -O3):

.LC0:
        .string "Hello world"
main:
        sub     rsp, 8
        mov     edx, 11
        mov     esi, OFFSET FLAT:.LC0
        mov     edi, OFFSET FLAT:_ZSt4cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
        mov     edi, OFFSET FLAT:_ZSt4cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)
        xor     eax, eax
        add     rsp, 8
        ret
_GLOBAL__sub_I_main:
        sub     rsp, 8
        mov     edi, OFFSET FLAT:_ZStL8__ioinit
        call    std::ios_base::Init::Init() [complete object constructor]
        mov     edx, OFFSET FLAT:__dso_handle
        mov     esi, OFFSET FLAT:_ZStL8__ioinit
        mov     edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
        add     rsp, 8
        jmp     __cxa_atexit

... что намного больше.

Известно, что в C ++ вы платите за то, что едите. В таком случае, за что я плачу?


3
Комментарии не для расширенного обсуждения; этот разговор был перенесен в чат .
Самуэль Лью


26
Никогда не слышал термин, eatсвязанный с C ++. Я полагаю, вы имеете в виду: «Вы платите только за то, что вы используете »?
Джакомо Альзетта

7
@GiacomoAlzetta, ... это разговорный язык, в котором используется концепция шведского стола, в котором все можно съесть. Использование более точного термина, безусловно, предпочтительнее для мировой аудитории, но как носитель английского языка для коренных американцев, название имеет смысл для меня.
Чарльз Даффи

5
@ trolley813 Утечки памяти не имеют ничего общего с цитатой и вопросом OP. Смысл «Вы платите только за то, что используете» / «Вы не платите за то, что не используете», означает, что никакого снижения производительности не происходит, если вы не используете конкретную функцию / абстракцию. Утечки памяти вообще не имеют к этому никакого отношения, и это только показывает, что этот термин eatболее двусмысленный и его следует избегать.
Джакомо Альзетта

Ответы:


60

Вы платите за то, чтобы вызывать тяжелую библиотеку (не такую ​​тяжелую, как печать в консоли). Вы инициализируете ostreamобъект. Есть несколько скрытых хранилищ. Затем вы звоните, std::endlкоторый не является синонимом для \n. iostreamБиблиотека позволяет регулировать множество параметров и положить нагрузку на процессор , а не программиста. Это то, за что вы платите.

Давайте рассмотрим код:

.LC0:
        .string "Hello world"
main:

Инициализация объекта ostream + cout

    sub     rsp, 8
    mov     edx, 11
    mov     esi, OFFSET FLAT:.LC0
    mov     edi, OFFSET FLAT:_ZSt4cout
    call    std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)

Повторный вызов coutдля печати новой строки и сброса

    mov     edi, OFFSET FLAT:_ZSt4cout
    call    std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)
    xor     eax, eax
    add     rsp, 8
    ret

Инициализация статического хранилища:

_GLOBAL__sub_I_main:
        sub     rsp, 8
        mov     edi, OFFSET FLAT:_ZStL8__ioinit
        call    std::ios_base::Init::Init() [complete object constructor]
        mov     edx, OFFSET FLAT:__dso_handle
        mov     esi, OFFSET FLAT:_ZStL8__ioinit
        mov     edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
        add     rsp, 8
        jmp     __cxa_atexit

Кроме того, важно различать язык и библиотеку.

Кстати, это только часть истории. Вы не знаете, что написано в функциях, которые вы вызываете.


5
В качестве дополнительного примечания, тщательное тестирование покажет, что добавление программы на C ++ с помощью «ios_base :: sync_with_stdio (false);» и "cin.tie (NULL)"; сделает cout быстрее, чем printf (Printf имеет служебную строку формата). Первое исключает накладные расходы из проверки cout; printf; coutправильности записи (поскольку они имеют свои собственные буферы). Второй будет рассинхронизирован coutи cin, в результате, cout; cinпотенциально будет запрашивать у пользователя информацию в первую очередь. Промывка заставит синхронизироваться только тогда, когда вам это действительно нужно.
Николас Пипитоне

Привет Николас, большое спасибо за добавление этих полезных заметок.
Араш

«важно различать язык и библиотеку»: ну да, но стандартная библиотека, поставляемая с языком, является единственной доступной везде, поэтому она используется везде (и да, стандартная библиотека C является частью спецификации C ++, поэтому она может быть использована при желании). Что касается «Вы не знаете, что написано в функциях, которые вы вызываете»: вы можете ссылаться статически, если действительно хотите знать, и действительно, вызывающий код, который вы исследуете, вероятно, не имеет значения.
Питер - восстановить Монику

211

В таком случае, за что я плачу?

std::cout более мощный и сложный, чем printf . Он поддерживает такие вещи, как локали, флаги форматирования с сохранением состояния и многое другое.

Если они вам не нужны, используйте std::printfили std::puts- они доступны в <cstdio>.


Известно, что в C ++ вы платите за то, что едите.

Я также хочу прояснить, что C ++ ! = Стандартная библиотека C ++. Стандартная библиотека должна быть универсальной и «достаточно быстрой», но часто она будет медленнее, чем специализированная реализация того, что вам нужно.

С другой стороны, язык C ++ стремится сделать возможным написание кода без дополнительных ненужных скрытых затрат (например, подписка virtual, сбор мусора).


4
+1 за то, что стандартная библиотека должна быть универсальной и «достаточно быстрой», но часто она будет медленнее, чем специализированная реализация того, что вам нужно. Многие, кажется, беспечно используют компоненты STL, не принимая во внимание последствия для производительности по сравнению с собственными.
Крейг Эстей

7
@Craig OTOH многие части стандартной библиотеки обычно быстрее и правильнее, чем то, что можно было бы вместо этого создать.
Питер - восстановить Монику

2
@ PeterA.Schneider OTOH, когда версия STL медленнее, чем в 20х-30х раз, качать свою собственную - это хорошо. Смотрите мой ответ здесь: codereview.stackexchange.com/questions/191747/… В нем другие также предложили [хотя бы частичное] свернуть свое.
Крейг Эстей

1
@CraigEstey Вектор (кроме начального динамического выделения, которое может быть значительным, в зависимости от того, сколько работы будет выполнено в конечном итоге с данным экземпляром) не менее эффективен, чем массив C; это разработано, чтобы не быть. Необходимо соблюдать осторожность, чтобы не копировать его, не зарезервировать достаточно места изначально и т. Д., Но все это также должно быть сделано с массивом, причем менее безопасно. Что касается вашего связанного примера: Да, вектор векторов будет (если не оптимизирован) подвергаться дополнительной косвенности по сравнению с двумерным массивом, но я предполагаю, что эффективность в 20 раз коренится не в этом, а в алгоритме.
Питер - восстановить Монику

174

Вы не сравниваете C и C ++. Вы сравниваете printfи std::cout, на которые способны разные вещи (локали, форматирование с учетом состояния и т. Д.).

Попробуйте использовать следующий код для сравнения. Godbolt генерирует одинаковую сборку для обоих файлов (протестировано с gcc 8.2, -O3).

main.c:

#include <stdio.h>

int main()
{
    int arr[6] = {1, 2, 3, 4, 5, 6};
    for (int i = 0; i < 6; ++i)
    {
        printf("%d\n", arr[i]);
    }
    return 0;
}

main.cpp:

#include <array>
#include <cstdio>

int main()
{
    std::array<int, 6> arr {1, 2, 3, 4, 5, 6};
    for (auto x : arr)
    {
        std::printf("%d\n", x);
    }
}


Приветствия за показ эквивалентного кода и объяснение причины.
HackSlash

134

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

Давайте проверим, что на самом деле делает ваш код:

C:

  • распечатать одну строку, "Hello world\n"

C ++:

  • поток строки "Hello world"вstd::cout
  • поток std::endlманипулятора вstd::cout

Очевидно, ваш код C ++ выполняет вдвое больше работы. Для честного сравнения мы должны объединить это:

#include <iostream>

int main()
{
    std::cout<<"Hello world\n";
    return 0;
}

… И вдруг ваш ассемблерный код mainвыглядит очень похожим на C:

main:
        sub     rsp, 8
        mov     esi, OFFSET FLAT:.LC0
        mov     edi, OFFSET FLAT:_ZSt4cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
        xor     eax, eax
        add     rsp, 8
        ret

Фактически, мы можем сравнивать код C и C ++ построчно, и различий очень мало :

sub     rsp, 8                      sub     rsp, 8
mov     edi, OFFSET FLAT:.LC0   |   mov     esi, OFFSET FLAT:.LC0
                                >   mov     edi, OFFSET FLAT:_ZSt4cout
call    puts                    |   call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
xor     eax, eax                    xor     eax, eax
add     rsp, 8                      add     rsp, 8
ret                                 ret

Единственная реальная разница в том, что в C ++ мы вызываем operator <<с двумя аргументами ( std::coutи строкой). Мы могли бы устранить даже эту небольшую разницу, используя более близкий C eqivalent:, fprintfкоторый также имеет первый аргумент, указывающий поток.

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

Но, как объясняется в других ответах, соответствующая разница между этими двумя программами не будет обнаружена в результатах сборки mainфункции, поскольку все тяжелые работы происходят за кулисами.


21
Кстати, среда выполнения C также должна быть настроена, и это происходит в вызываемой функции, _startно ее код является частью библиотеки времени выполнения C. В любом случае это происходит как для C, так и для C ++.
Конрад Рудольф

2
@Deduplicator: На самом деле, по умолчанию библиотека iostream не делает никакой буферизации std::coutи вместо проходит I / O в STDIO реализации (который использует свои собственные механизмы буферизации). В частности, при подключении к (как известно, к) интерактивному терминалу, по умолчанию вы никогда не увидите полностью буферизованный вывод при записи std::cout. Вы должны явно отключить синхронизацию с stdio, если хотите, чтобы библиотека iostream использовала свои собственные механизмы буферизации std::cout.

6
@KonradRudolph: На самом деле, нет printfнеобходимости очищать потоки здесь. Фактически, в обычном случае использования (вывод перенаправлен в файл), вы обычно обнаружите, что printfоператор не сбрасывается. Только когда выход буферизован или небуферизован, printfтриггер будет сброшен.

2
@PeterCordes: Точно, вы не можете блокировать с незапущенными выходными буферами, но вы можете удивиться, когда программа приняла ваш ввод и продолжила работу, не отображая ожидаемый результат. Я знаю это, потому что у меня была возможность отладить «Справка, моя программа зависает во время ввода, но я не могу понять, почему!» это дало другому разработчику приступы в течение нескольких дней.

2
@PeterCordes: Аргумент, который я делаю, - «напиши, что ты имеешь в виду» - новые строки подходят, когда вы имеете в виду, что выходные данные будут в конечном итоге доступны, и endl уместны, когда вы хотите, чтобы выходные данные были доступны немедленно.

53

Известно, что в C ++ вы платите за то, что едите. В таком случае, за что я плачу?

Это просто Вы платите за std::cout. «Вы платите только за то, что едите», не означает «вы всегда получаете лучшие цены». Конечно, printfдешевле. Можно утверждать, что std::coutэто безопаснее и универсальнее, поэтому его большая стоимость оправдана (она стоит дороже, но обеспечивает большую ценность), но это упускает из виду. Вы не используете printf, вы используете std::cout, поэтому вы платите за использование std::cout. Вы не платите за использование printf.

Хороший пример - виртуальные функции. Виртуальные функции требуют определенных затрат времени и места, но только если вы их действительно используете. Если вы не используете виртуальные функции, вы ничего не платите.

Несколько замечаний

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

  2. На самом деле, иногда это даже лучше, чем «в C ++ вы платите за то, что едите». Например, компилятор может сделать вывод, что вызов виртуальной функции не требуется в некоторых обстоятельствах, и преобразовать его в не виртуальный вызов. Это означает, что вы можете получить виртуальные функции бесплатно . Разве это не здорово?


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

2
@alephzero Я не уверен, что особенно уместно сравнивать затраты на разработку с затратами на производительность.

Такая прекрасная возможность для каламбура ... Вы могли бы использовать слово «калории» вместо «цена». Исходя из этого, можно сказать, что C ++ толще, чем C. Или, по крайней мере ... конкретный рассматриваемый код (я склонен к C ++ в пользу C, поэтому я не могу честно выйти за рамки). Увы. @Bilkokuya Это может быть не во всех случаях, но это, безусловно, то, что не стоит игнорировать. Таким образом, это актуально в целом.
Pryftan

46

«Список сборок для printf» НЕ для printf, а для put (тип оптимизации компилятора?); printf гораздо сложнее, чем puts ... не забывайте!


13
Это пока лучший ответ, так как все остальные зацикливаются на красной селедке о std::coutвнутренностях, которые не видны в списке сборки.
Конрад Рудольф

12
Список сборок предназначен для вызова puts , который выглядит идентично вызову, printfесли вы передаете только одну строку формата и ноль лишних аргументов. (за исключением того, что также будет xor %eax,%eaxпричина, потому что мы передаем нулевые аргументы FP в регистрах в функцию с переменным числом.) Ни одна из них не является реализацией, просто передачей указателя на строку в библиотечную функцию. Но да, оптимизация printfдля puts- это то, что gcc делает для форматов, которые имеют только "%s"или когда нет преобразований, и строка заканчивается новой строкой.
Питер Кордес

45

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

Перейдите к приведенной ниже сводке, чтобы найти ответ на свой главный вопрос, если вы не хотите проходить через всю эту стену текста.


абстракция

В таком случае, за что я плачу?

Вы платите за абстракцию . Возможность писать более простой и понятный для человека код обходится дорого. В C ++, который является объектно-ориентированным языком, почти все является объектом. Когда вы используете какой-либо предмет, под капотом всегда будут происходить три главных вещи:

  1. Создание объекта, в основном выделение памяти для самого объекта и его данных.
  2. Инициализация объекта (обычно с помощью какого-либо init()метода). Обычно выделение памяти происходит под капотом, как первое на этом этапе.
  3. Разрушение объекта (не всегда).

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

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

Что на самом деле происходит в C ++?

Вот оно, сломано:

  1. std::ios_baseКласс инициализируется, который является базовым классом для всего I / O связаны между собой .
  2. std::coutОбъект инициализируется.
  3. Ваша строка загружается и передается std::__ostream_insert, что (как вы уже поняли по названию) является методом std::cout(в основном <<оператором), который добавляет строку в поток.
  4. cout::endlтакже передается std::__ostream_insert.
  5. __std_dso_handleпередаются __cxa_atexit, что глобальная функция , которая отвечает за «чистку» перед выходом из программы. __std_dso_handleсама эта функция вызывается для освобождения и уничтожения оставшихся глобальных объектов.

Так что использование C == ничего не платит?

В коде C происходит очень мало шагов:

  1. Ваша строка загружается и передается putsчерез ediреестр.
  2. puts вызывается.

Нет объектов нигде, следовательно, не нужно ничего инициализировать / уничтожать.

Это, однако, не означает, что вы ничего не «платите» в Си . Вы по-прежнему платите за абстракцию, а также за инициализацию стандартной библиотеки C и динамическое разрешение, при котором printfфункция (или, собственно puts, оптимизированная компилятором, поскольку вам не нужна строка формата) все еще происходит под капотом.

Если бы вы написали эту программу в чистом виде, она бы выглядела примерно так:

jmp start

msg db "Hello world\n"

start:
    mov rdi, 1
    mov rsi, offset msg
    mov rdx, 11
    mov rax, 1          ; write
    syscall
    xor rdi, rdi
    mov rax, 60         ; exit
    syscall

Что в основном приводит только к write вызову системного вызова, за которым следует exitсистемный вызов. Теперь это был бы минимум для достижения того же.


Подвести итоги

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

Отвечая на ваш главный вопрос :

Я плачу за то, что не ем?

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


Ох, и еще одна вещь!

На первый взгляд преимущества C ++ могут показаться неочевидными, поскольку вы написали очень простую и небольшую программу, но посмотрите на более сложный пример и увидите разницу (обе программы делают одно и то же):

C :

#include <stdio.h>
#include <stdlib.h>

int cmp(const void *a, const void *b) {
    return *(int*)a - *(int*)b;
}

int main(void) {
    int i, n, *arr;

    printf("How many integers do you want to input? ");
    scanf("%d", &n);

    arr = malloc(sizeof(int) * n);

    for (i = 0; i < n; i++) {
        printf("Index %d: ", i);
        scanf("%d", &arr[i]);
    }

    qsort(arr, n, sizeof(int), cmp)

    puts("Here are your numbers, ordered:");

    for (i = 0; i < n; i++)
        printf("%d\n", arr[i]);

    free(arr);

    return 0;
}

C ++ :

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

int main(void) {
    int n;

    cout << "How many integers do you want to input? ";
    cin >> n;

    vector<int> vec(n);

    for (int i = 0; i < vec.size(); i++) {
        cout << "Index " << i << ": ";
        cin >> vec[i];
    }

    sort(vec.begin(), vec.end());

    cout << "Here are your numbers:" << endl;

    for (int item : vec)
        cout << item << endl;

    return 0;
}

Надеюсь, вы можете ясно увидеть, что я имею в виду здесь. Также обратите внимание на то, как в C вы должны управлять памятью на более низком уровне с помощью mallocи freeкак вам нужно быть более осторожным с индексированием и размерами, и как вы должны быть очень точными при вводе и печати.


27

Есть несколько заблуждений для начала. Во-первых, программа C ++ не приводит к 22 инструкциям, это больше похоже на 22 000 из них (я вытащил это число из своей шляпы, но оно примерно в поле). Кроме того, код C не дает 9 инструкций. Это только те, которые вы видите.

Что делает код C, так это то, что после выполнения многих вещей, которые вы не видите, он вызывает функцию из CRT (которая обычно, но не обязательно присутствует в качестве разделяемой библиотеки), а затем не проверяет возвращаемое значение или дескриптор. ошибки и выручает. В зависимости от компилятора и настроек оптимизации он даже не вызывается, printfно putsили что-то еще более примитивное.
Вы могли бы написать более или менее ту же самую программу (за исключением некоторых невидимых функций инициализации) также на C ++, если бы только вы вызывали ту же самую функцию таким же образом. Или, если вы хотите быть супер-корректным, используйте ту же функцию с префиксом std::.

Соответствующий код C ++ на самом деле совсем не одно и то же. В то время как все <iostream>это хорошо известно как толстый уродливый поросенок, который добавляет огромные накладные расходы для небольших программ (в «настоящей» программе вы не особо замечаете этого), несколько более справедливая интерпретация заключается в том, что это ужасно много вещей, которые вы не видите и которые просто работают . Включая, но не ограничиваясь этим, магическое форматирование практически любого случайного материала, включая различные числовые форматы и локали и тому подобное, буферизацию и надлежащую обработку ошибок. Обработка ошибок? Ну да, угадайте, что, вывод строки может действительно потерпеть неудачу, и в отличие от программы C, программа C ++ не будет игнорировать это молча. Учитывая чтоstd::ostreamделает под капотом, и никто не узнает, это на самом деле довольно легкий. Не то, чтобы я использовал это, потому что я ненавижу потоковый синтаксис со страстью. Но все же, это довольно круто, если учесть, что он делает.

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

Если вместо того, чтобы «запустить и забыть надежду на лучшее», вы хотите написать правильный код C (т. Е. Вы на самом деле проверяете на наличие ошибок, и программа ведет себя правильно при наличии ошибок), тогда разница незначительна, если есть.


16
Очень хороший ответ, за исключением того, что это утверждение: «Но, конечно, C ++ в целом не так эффективен, как C», просто ошибочно. C ++ может быть столь же эффективным, как C, и достаточно высокоуровневый код может быть более эффективным, чем эквивалентный C-код. Да, C ++ имеет некоторые издержки из-за необходимости обрабатывать исключения, но в современных компиляторах эти издержки незначительны по сравнению с выигрышем в производительности от лучших бесплатных абстракций.
Конрад Рудольф

Если я правильно понял, тоже std::coutвыкидывает исключения?
Сахер

6
@Saher: Да, нет, может быть. std::coutэто то, std::basic_ostreamчто можно выбросить, и он может перебрасывать возникающие в противном случае исключения, если настроен на это, или он может проглотить исключения. Дело в том, что вещи могут потерпеть неудачу, и C ++, а также стандартная библиотека C ++ (в основном) созданы, поэтому сбои не могут остаться незамеченными. Это раздражение и благословение (но скорее благословение, чем раздражение). С другой стороны, просто показывает средний палец. Вы не проверяете код возврата, вы никогда не знаете, что случилось.
Деймон

1
@KonradRudolph: Да, это то, на что я пытался указать: «Я редко обнаруживал, что C ++ работает лучше, потому что по тем или иным причинам он дает более благоприятные оптимизации. Не спрашивайте меня, почему именно» , Не сразу понятно почему, но нередко это просто оптимизирует лучше. Для любой причины. Можно подумать, что оптимизатору все равно, но это не так.
Деймон

22

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

<Iomanip>

Наиболее отвратительной частью потока C ++ io api является наличие этой библиотеки заголовков форматирования. Помимо сохранения состояния, уродства и ошибок, он объединяет форматирование с потоком.

Предположим, что вы хотите распечатать строку с 8-значным нулем, заполненным шестнадцатеричным int без знака, за которым следует пробел, за которым следует двойное число с 3 десятичными знаками. С помощью <cstdio>вы получите краткую строку формата. С помощью <ostream>, вы должны сохранить старое состояние, установить выравнивание вправо, установить символ заливки, установить ширину заливки, установить основание на шестнадцатеричное, вывести целое число, восстановить сохраненное состояние (в противном случае ваше целочисленное форматирование будет загрязнять форматирование с плавающей запятой), вывести пробел установите фиксированную запись, задайте точность, выведите двойную и новую строки, а затем восстановите старое форматирование.

// <cstdio>
std::printf( "%08x %.3lf\n", ival, fval );

// <ostream> & <iomanip>
std::ios old_fmt {nullptr};
old_fmt.copyfmt (std::cout);
std::cout << std::right << std::setfill('0') << std::setw(8) << std::hex << ival;
std::cout.copyfmt (old_fmt);
std::cout << " " << std::fixed << std::setprecision(3) << fval << "\n";
std::cout.copyfmt (old_fmt);

Перегрузка оператора

<iostream> Является ли плакат потомком того, как не использовать перегрузку операторов:

std::cout << 2 << 3 && 0 << 5;

Производительность

std::coutв несколько раз медленнее printf(). Безудержный фурит и виртуальная отправка делают свое дело.

Поток безопасности

Оба <cstdio>и <iostream>потокобезопасны в том смысле, что каждый вызов функции является атомарным. Но, printf()гораздо больше сделано за звонок. Если вы запустите следующую программу с <cstdio>опцией, вы увидите только строку f. Если вы используете <iostream>многоядерный компьютер, вы, скорее всего, увидите что-то еще.

// g++ -Wall -Wextra -Wpedantic -pthread -std=c++17 cout.test.cpp

#define USE_STREAM 1
#define REPS 50
#define THREADS 10

#include <thread>
#include <vector>

#if USE_STREAM
    #include <iostream>
#else
    #include <cstdio>
#endif

void task()
{
    for ( int i = 0; i < REPS; ++i )
#if USE_STREAM
        std::cout << std::hex << 15 << std::dec;
#else
        std::printf ( "%x", 15);
#endif

}

int main()
{
    auto threads = std::vector<std::thread> {};
    for ( int i = 0; i < THREADS; ++i )
        threads.emplace_back(task);

    for ( auto & t : threads )
        t.join();

#if USE_STREAM
        std::cout << "\n<iostream>\n";
#else
        std::printf ( "\n<cstdio>\n" );
#endif
}

Ответ на этот пример заключается в том, что большинство людей применяют дисциплину, чтобы в любом случае никогда не записывать один дескриптор файла из нескольких потоков. Ну, в этом случае, вы должны будете заметить, что это <iostream>поможет захватить блокировку на всех <<и каждого >>. Принимая во внимание <cstdio>, что вы не будете блокировать так часто, и у вас даже есть возможность не блокировать.

<iostream> расходует больше блокировок для достижения менее последовательного результата.


2
Большинство реализаций printf имеют чрезвычайно полезную функцию для локализации: пронумерованные параметры. Если вам нужно произвести вывод на двух разных языках (например, на английском и французском) и порядок слов различен, вы можете использовать один и тот же printf с другой строкой форматирования, и он может печатать параметры в другом порядке.
gnasher729

2
Такое форматированное состояние потоков, должно быть, дало так много трудностей в поиске ошибок, что я не знаю, что сказать. Отличный ответ. Проголосовал бы не раз, если бы мог.
mathreadler

6
«В std::coutнесколько раз медленнее printf()» - это утверждение повторяется по всей сети, но оно не было верным в течение веков. Современные реализации IOstream работают наравне с printf. Последний также выполняет виртуальную диспетчеризацию для внутренних операций с буферизованными потоками и локализованным вводом-выводом (выполняется операционной системой, но тем не менее).
Конрад Рудольф

3
@KevinZ И это здорово, но он сравнивает один конкретный вызов, который демонстрирует сильные стороны fmt (множество разных форматов в одной строке). При более типичном использовании разница между printfи coutуменьшается. Кстати, на этом сайте есть множество таких тестов.
Конрад Рудольф

3
@KonradRudolph Это тоже не правда. Микробенчмарки часто недооценивают стоимость раздувания и косвенного обращения, потому что они не исчерпывают определенные ограниченные ресурсы (будь то регистры, icache, память, предикторы ветвления), где будет настоящая программа. Когда вы намекаете на «более типичное использование», это, в основном, говорит о том, что у вас гораздо больше раздувания в других местах, что нормально, но не по теме. На мой взгляд, если у вас нет требований к производительности, вам не нужно программировать на C ++.
KevinZ

18

В дополнение к тому, что сказали все остальные ответы,
есть еще тот факт, что std::endlэто не то же самое, что '\n'.

Это, к сожалению, распространенное заблуждение. std::endlне означает «новая строка»,
это означает «напечатать новую строку и затем очистить поток ». Промывка не дешевая!

Полностью игнорируя различия между printfи std::coutна мгновение, чтобы быть функционально эквивалентным вашему примеру C, ваш пример C ++ должен выглядеть следующим образом:

#include <iostream>

int main()
{
    std::cout << "Hello world\n";
    return 0;
}

А вот пример того, какими должны быть ваши примеры, если вы включите сброс.

С

#include <stdio.h>

int main()
{
    printf("Hello world\n");
    fflush(stdout);
    return 0;
}

C ++

#include <iostream>

int main()
{
    std::cout << "Hello world\n";
    std::cout << std::flush;
    return 0;
}

Когда вы сравниваете код, вы всегда должны быть осторожны, чтобы сравнивать «лайки» и понимать смысл того, что делает ваш код. Иногда даже самые простые примеры сложнее, чем думают некоторые люди.


На самом деле использование std::endl - это функциональный эквивалент записи новой строки в поток stdio с буферизацией строки. stdoutв частности, требуется, чтобы он был линейно-буферизован или не буферизован при подключении к интерактивному устройству. Я полагаю, что Linux настаивает на использовании линейного буфера.

На самом деле, библиотека iostream не имеет режима буферизации строки ... способ достижения эффекта буферизации строки состоит в том, чтобы использовать std::endlвывод строки.

@ Hurkyl Настаивать? Тогда какая польза от этого setvbuf(3)? Или вы хотите сказать, что по умолчанию используется линейная буферизация? К вашему сведению: обычно все файлы имеют блочную буферизацию. Если поток ссылается на терминал (как обычно делает stdout), он буферизуется в строке. Стандартный поток ошибок stderr по умолчанию всегда небуферизован.
Прифтан

Не printfмигает автоматически при встрече с символом новой строки?
bool3max

1
@ bool3max Это только скажет мне, что делает моя среда, она может отличаться в других средах. Даже если он ведет себя одинаково во всех самых популярных реализациях, это не значит, что где-то есть крайний случай. Вот почему стандарт так важен - стандарт диктует, должно ли что-то быть одинаковым для всех реализаций или может ли оно варьироваться между реализациями.
Pharap

16

Хотя существующие технические ответы верны, я думаю, что вопрос в конечном счете вытекает из этого заблуждения:

Известно, что в C ++ вы платите за то, что едите.

Это просто маркетинговый разговор сообщества C ++. (Справедливости ради, в каждом языковом сообществе говорят о маркетинге.) Это не означает ничего конкретного, от чего можно серьезно зависеть.

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

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


1
Я могу подумать только о двух вещах, когда вы платите за то, что вы не используете: исключения и RTTI. И я не думаю, что это маркетинговые разговоры; C ++ по сути является более мощным C, который также «не платит за то, что вы используете».
Rakete1111

2
@ Rakete1111 Давно установлено, что если исключения не выдают, они не стоят. Если ваша программа генерирует последовательно, она должна быть переработана. Если условие сбоя находится вне вашего контроля, вы должны проверить его с помощью проверки bool, возвращающей здравомыслие, прежде чем вызывать метод, основанный на ложном условии условия.
Шульмастер

1
@schulmaster: Исключения могут накладывать конструктивные ограничения, когда код, написанный на C ++, должен взаимодействовать с кодом, написанным на других языках, поскольку нелокальные передачи управления могут работать плавно только между модулями, если модули знают, как координировать действия друг с другом.
суперкат

1
(хотя, возможно, не все) языки стремятся быть эффективными . Определенно не все: эзотерические языки программирования стремятся быть новыми / интересными, а не эффективными. esolangs.org . Некоторые из них, такие как BrainFuck, совершенно неэффективны. Или, например, язык программирования Шекспира, минимальный размер 227 байт (codegolf) для печати всех целых чисел . Из языков, предназначенных для производственного использования, большинство стремятся к эффективности, но некоторые (например, bash) стремятся в основном для удобства и, как известно, работают медленно.
Питер Кордес

2
Ну, это маркетинг, но это почти полностью верно. Вы можете придерживаться <cstdio>и не включать <iostream>, так же, как вы можете скомпилировать -fno-exceptions -fno-rtti -fno-unwind-tables -fno-asynchronous-unwind-tables.
KevinZ

11

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

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

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


23
«Функции ввода / вывода в C ++ - это отвратительные монстры, изо всех сил пытающиеся скрыть свою ктульскую природу за тонким слоем полезности. Во многих отношениях они являются демонстрацией того, как не разрабатывать современный код C ++». Возможно, будет более точным.
user673679

3
@ user673679: Очень верно. Большая проблема с потоками ввода / вывода C ++ заключается в том, что под ней: действительно происходит много сложностей, и любой, кто когда-либо имел дело с ними (я обращаюсь к std::basic_*streamнисходящему), знает входящие eadaches. Они были разработаны, чтобы быть широко общими и распространяться через наследование; но никто в конце концов не сделал этого из-за их сложности (буквально на iostreams написаны книги), настолько, что новые библиотеки были созданы именно для этого (например, boost, ICU и т. д.). Я сомневаюсь, что мы когда-нибудь перестанем платить за эту ошибку.
edmz

1

Как вы видели в других ответах, вы платите, когда ссылаетесь в общие библиотеки и вызываете сложные конструкторы. Здесь нет никакого особого вопроса, больше жалобы. Я укажу некоторые реальные аспекты:

  1. У Барна был основной принцип проектирования, который никогда не позволял эффективности быть причиной оставаться в C, а не в C ++. Тем не менее, нужно быть осторожным, чтобы получить эти эффективности, и есть случайные эффективности, которые всегда работали, но не были «технически» в спецификации C. Например, расположение битовых полей на самом деле не было указано.

  2. Попробуйте посмотреть через ostream. Боже мой, это раздутый! Я не удивлюсь, если найду симулятор полета там. Даже stdlib printf () обычно работает около 50K. Это не ленивые программисты: половина размера printf была связана с косвенными аргументами точности, которые большинство людей никогда не используют. Почти каждая действительно ограниченная библиотека процессора создает свой собственный выходной код вместо printf.

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

  4. Люди все еще пишут ANSI C, хотя редко K & R C. Мой опыт показывает, что мы всегда компилируем его с помощью компилятора C ++, используя несколько настроек, чтобы ограничить то, что перетаскивается. Для других языков есть хорошие аргументы: Go удаляет полиморфные издержки и сумасшедший препроцессор ; было несколько хороших аргументов в пользу более разумной упаковки полей и размещения памяти. ИМХО Я думаю, что любой языковой дизайн должен начинаться с перечисления целей, во многом как Zen of Python .

Это была веселая дискуссия. Вы спрашиваете, почему у вас не может быть волшебно маленьких, простых, элегантных, полных и гибких библиотек?

Там нет ответа. Там не будет ответа. Это ответ.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.