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


170

Розглянемо наступні привітні світові приклади в 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 для складання, розмір коду С становить лише 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

... що набагато більше.

Відомо, що в С ++ ви платите за те, що їсте. Отже, у такому випадку, за що я плачу?


3
Коментарі не для розширеного обговорення; ця розмова була переміщена до чату .
Самуель Liew


26
Ніколи не чув термін, eatпов’язаний із C ++. Я вважаю, ви маєте на увазі: "Ви платите лише за те, що використовуєте "?
Джакомо Альзетта

7
@GiacomoAlzetta, ... це розмова, застосовуючи концепцію шведського столу, який можна їсти. Використання більш точного терміна, безумовно, є кращим для світової аудиторії, але як американський носій англійської мови, назва має для мене сенс.
Чарльз Даффі

5
@ trolley813 Витоки пам’яті не мають нічого спільного з цитатою та питанням щодо ОП. Точка "Ви платите лише за те, що використовуєте" / "Ви не платите за те, що не використовуєте", - це сказати, що жодне звернення до ефективності не приймається, якщо ви не використовуєте певну особливість / абстракцію. Витоки пам’яті взагалі нічого спільного з цим не мають, і це лише показує, що термін eatє більш неоднозначним і його слід уникати.
Джакомо Альзетта

Відповіді:


60

За що ви платите, це зателефонувати у важку бібліотеку (не настільки важка, як друк на консолі). Ви ініціалізуєтеostream об'єкт. Є кілька прихованих сховищ. Потім ви називаєте, std::endlщо не є синонімом \n. iostreamБібліотека дозволяє регулювати безліч параметрів і покласти навантаження на процесор , а не програміста. Це те, за що ти платиш.

Давайте розглянемо код:

.LC0:
        .string "Hello world"
main:

Ініціалізація потокового об'єкта + 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 ++ прагне дати можливість писати код, не платячи зайві додаткові приховані витрати (наприклад, відмова virtual, відсутність збору сміття).


4
+1, сказавши, що Стандартна бібліотека повинна бути загальноприйнятою та "досить швидкою", але це буде часто повільніше, ніж спеціалізована реалізація того, що вам потрібно. Багато хто, здається, з легкістю використовують компоненти STL, не враховуючи наслідків для продуктивності та прокатки власних.
Крейг Естей

7
@Craig OTOH багато частин стандартної бібліотеки зазвичай швидше і правильніше, ніж те, що можна створити замість цього.
Пітер - Відновіть Моніку

2
@ PeterA.Schneider OTOH, коли версія STL на 20x30x повільніше, прокат власних - це добре. Дивіться мою відповідь тут: codereview.stackexchange.com/questions/191747/… Там же інші також запропонували [принаймні частково] запустити свій власний.
Крейг Есті

1
@CraigEstey Вектор (крім початкового динамічного розподілу, який може бути значним, залежно від того, скільки роботи буде виконано в кінцевому підсумку з даним екземпляром), не менш ефективний, ніж масив C; це покликано не бути. Потрібно стежити за тим, щоб не копіювати його навколо, спочатку зарезервуйте достатньо місця тощо, але все це потрібно зробити і з масивом, і менш безпечно. Що стосується вашого зв'язаного прикладу: Так, вектор векторів (якщо не буде оптимізовано) матиме додаткове непряме порівняння з двовимірним масивом, але я припускаю, що ефективність 20x не вкорінена там, а в алгоритмі.
Пітер - Відновіть Моніку

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

Відомо, що в С ++ ви платите за те, що їсте. Отже, у такому випадку, за що я плачу?

Це просто. Ви платите std::cout. "Ви платите лише за те, що їсте" не означає, що "ви завжди отримуєте найкращі ціни". Звичайно, printfдешевше. Можна стверджувати, що std::coutце безпечніше і універсальніше, тому більша його вартість виправдана (вона коштує більше, але забезпечує більшу цінність), але це не вистачає суті. Ви не використовуєте printf, ви використовуєте std::cout, тому ви платите за користування std::cout. Ви не платите за користування printf.

Хороший приклад - віртуальні функції. Віртуальні функції мають певні вимоги до витрат і простору, але лише якщо ви їх фактично використовуєте. Якщо ви не використовуєте віртуальні функції, ви нічого не платите.

Кілька зауважень

  1. Навіть якщо C ++ код оцінює більше інструкцій по збірці, це все-таки кілька інструкцій, і будь-які накладні витрати все ще, ймовірно, карбуються фактичними операціями вводу / виводу.

  2. Насправді іноді це навіть краще, ніж "у С ++ ви платите за те, що їсте". Наприклад, компілятор може встановити, що виклик віртуальної функції в деяких обставинах не потрібен, і перетворити його в невіртуальний виклик. Це означає , що ви можете отримати віртуальні функції для безкоштовно . Хіба це не чудово?


6
Ви не отримуєте віртуальних функцій безкоштовно. Ви все одно повинні заплатити витрати, спочатку написавши їх, а потім налагодити трансформацію коду компілятора, коли він не відповідає вашому уявленню про те, що він повинен був робити.
alephzero

2
@alephzero Я не впевнений, що це особливо актуально для порівняння витрат на розробку з витратами на продуктивність.

Така чудова можливість для каламбура була втрачена ... Ви могли використовувати слово "калорії" замість "ціна". З цього ви можете сказати, що C ++ товстіший за C. Або принаймні ... конкретний код, про який йдеться (я упереджений щодо C ++ на користь C, тому я не можу справедливо вийти за рамки). На жаль @Bilkokuya Це може бути не актуальним у всіх випадках, але це, безумовно, щось, що не слід нехтувати. Таким чином, це актуально в цілому.
Прифтан

46

"Перелік складання для printf" НЕ для printf, а для put (вид оптимізації компілятора?); printf набагато складніше, ніж ставить ... не забувайте!


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

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

45

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

Перейдіть до наведеного нижче резюме, щоб отримати відповідь на своє головне питання, якщо ви не хочете пройти всю цю текстову стіну.


Абстракція

Отже, у такому випадку, за що я плачу?

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

  1. Створення об'єкта, в основному розподіл пам'яті для самого об'єкта та його даних.
  2. Ініціалізація об'єкта (зазвичай за допомогою якогось init()методу). Зазвичай розподіл пам'яті відбувається під кришкою, як перше, на цьому кроці.
  3. Руйнування об'єкта (не завжди).

Ви не бачите його в коді, але кожен раз, коли ви використовуєте об'єкт, всі три вищезгадані речі повинні відбуватися якось. Якби ви все робили вручну, код, очевидно, був би набагато довшим.

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

Що насправді відбувається в C ++?

Ось вона, розбита:

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

Отже, використовуючи 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 syscall, за яким слід exitsyscall. Тепер це був би найменший мінімум, щоб здійснити те саме.


Узагальнити

C - це набагато голіше , і це лише мінімальний необхідний мінімум, залишаючи повний контроль користувачеві, який здатний повністю оптимізувати та налаштувати все, що завгодно. Ви говорите процесору завантажити рядок у реєстр, а потім викликаєте функцію бібліотеки, щоб використовувати цей рядок. З іншого боку, 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 з них (я витягнув це число з капелюха, але він приблизно знаходиться в бальній частині). Також код С не призводить до 9 інструкцій. Це лише ті, кого ти бачиш.

Що робить код C, це те, що він робить багато речей, які ви не бачите, він викликає функцію з CRT (яка зазвичай, але не обов'язково присутня як спільна lib), а потім не перевіряє повернене значення чи обробку помилки та виправлення. Залежно від параметрів компілятора та оптимізації, він навіть не викликає, printfале putsщось ще примітивніше.
Ви могли також написати більш-менш ту саму програму (за винятком деяких невидимих ​​функцій init) і в C ++, якби тільки та сама функція викликала ту саму функцію. Або, якщо ви хочете бути надто правильним, ця сама функція має префікс std::.

Відповідний код C ++ насправді зовсім не те саме. Хоча все <iostream>це добре відоме тим, що є товстою потворною свинею, яка додає величезних накладних витрат на невеликі програми (у "справжній" програмі ви насправді не так багато помічаєте), дещо справедливіша інтерпретація полягає в тому, що вона робить жахливою багато чого, що ви не бачите і який просто працює . Включаючи, але не обмежуючись, магічне форматування майже будь-яких випадкових речей, включаючи різні формати чисел та локалі та щось подібне, буферизацію та правильне поводження з помилками. Помилка обробки? Ну так, вгадайте, що, виведення рядка може насправді не вдатися, і на відміну від програми C, програма C ++ не буде ігнорувати це мовчки. Враховуючи, щоstd::ostreamробить під кришкою, і не знаючи нікого, це насправді досить легка вага. Не так, як я його використовую, бо ненавиджу синтаксис потоку із пристрастю. Але все ж, це досить приголомшливо, якщо врахувати, що це робить.

Але звичайно, C ++ в цілому не настільки ефективний, як C. Він не може бути настільки ефективним, оскільки це не те саме, і це не те саме. Якщо нічого іншого, C ++ створює винятки (і код для їх генерування, обробки або відмови), і він дає певні гарантії, які C не дає. Тож, звичайно, щось на зразок програми C ++ обов'язково має бути трохи більшим. Однак у великій картині це ніяк не має значення. Навпаки, по- справжньому програм я не рідко виявляв, що C ++ працює краще, тому що з тих чи інших причин, здається, надаються позитивніші оптимізації. Не питайте мене, чому, зокрема, я б не знав.

Якщо замість вогню і забути-сподіватися на найкраще ви дбаєте написати код C, який є правильним (тобто ви фактично перевіряєте наявність помилок, і програма поводиться правильно при наявності помилок), то різниця незначна, за наявності.


16
Дуже хороша відповідь, за винятком того, що це твердження: "Але, звичайно, C ++ загалом не настільки ефективний, як це може бути", просто помилково. C ++ може бути настільки ж ефективним, як C, і досить високий рівень коду може бути більш ефективним, ніж еквівалентний код C. Так, C ++ має деякі накладні витрати через те, що потрібно обробляти винятки, але на сучасних компіляторах витрачається на це мізерно, порівняно з підвищенням продуктивності від кращих безвитратних абстракцій.
Конрад Рудольф

Якщо я правильно зрозумів, чи std::coutкидають винятки також?
Сахер

6
@Saher: Так, ні, можливо. std::coutце std::basic_ostreamі те, що можна кидати, і воно може скинути винятки, що трапляються в іншому випадку, якщо це налаштовано так, або він може проковтнути винятки. Річ у тому, що речі можуть вийти з ладу, і C ++, а також стандартна лінійка C ++ побудовані (в основному), тому збої не проходять легко непоміченими. Це роздратування і благо (але, більше благословення, ніж роздратування). C з іншого боку, просто показує вам середній палець. Ви не перевіряєте код повернення, ніколи не знаєте, що сталося.
Деймон

1
@KonradRudolph: Правда, це те, що я намагався зазначити: "Я не рідко виявляв, що C ++ працює краще, тому що з тієї чи іншої причини, здається, надаю більш сприятливі оптимізації. Не запитуйте мене, чому саме" . Не відразу очевидно, чому, але не рідко це просто оптимізується краще. З будь-якої причини. Ви б могли подумати, що це все одно до оптимізатора, але це не так.
Деймон

22

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

<іоманіп>

Найвідповідальнішою частиною потоку 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(). Насичений Featuritis і віртуальна відправка дійсно має своє значення.

Безпека нитки

І те <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 Це теж не вірно. Мікро-орієнтири часто недооцінюють витрати на роздуття та непрямість, оскільки вони не вичерпують певних обмежених ресурсів (будь то регістри, ікаче, пам'ять, гілки прогнозів), де реальна програма буде. Коли ви натякаєте на "більш типове використання", це, по суті, говорить про те, що у вас в інших місцях значно сильніше, що добре, але поза темою. На мою думку, якщо у вас немає вимог до продуктивності, вам не потрібно програмувати на 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)? Або ти маєш на увазі сказати, що за замовчуванням буферний рядок? FYI: Зазвичай всі файли буферизовані блоком. Якщо потік посилається на термінал (як це робить stdout), він буферизований рядком. Стандартний потік помилок stderr завжди не буферизований за замовчуванням.
Прифтан

Не printfстикається автоматично, зустрічаючи символ нового рядка?
bool3max

1
@ bool3max Це підказало б мені лише те, що робить моє середовище, а в інших умовах може бути інакше. Навіть якщо він поводиться однаково у всіх найпопулярніших реалізаціях, це не означає, що десь є крайній випадок. Ось чому stanard настільки важливий - стандарт диктує, чи має бути щось однакове для всіх реалізацій або чи дозволено змінюватись між реалізаціями.
Фарап

16

Хоча існуючі технічні відповіді правильні, я вважаю, що питання в кінцевому підсумку випливає з цієї помилки:

Відомо, що в С ++ ви платите за те, що їсте.

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

"Ви платите за те, що використовуєте", означає, що функція C ++ має накладні витрати лише тоді, коли ви використовуєте цю функцію. Але визначення "ознаки" не є нескінченно детальним. Часто ви в кінцевому підсумку активуєте функції, які мають декілька аспектів, і хоча вам потрібен лише підмножина цих аспектів, часто реалізація цієї функції частково не практична або неможлива.

Загалом, багато (хоча і не всі) мови прагнуть бути ефективними з різним ступенем успіху. C ++ десь на масштабі, але в його дизайні немає нічого особливого чи магічного, що дозволило б досягти успіху в цій цілі.


1
Я думаю лише про дві речі, де ви платите за те, що не використовуєте: винятки та RTTI. І я не думаю, що це маркетингові розмови; C ++ в основному є більш потужним C, який також "не платять за те, що ви використовуєте".
Rakete1111

2
@ Rakete1111 Давно встановлено, що якщо винятки не кидаються, вони не коштують. Якщо ваша програма кидає послідовно, її слід переробити. Якщо стан відмови не піддається вашому контролю, вам слід перевірити стан за допомогою перевірки рівня безпеки, що повертає бул, перш ніж викликати метод, який покладається на те, що умова не відповідає дійсності.
schulmaster

1
@schulmaster: Винятки можуть накладати обмеження на дизайн, коли код, написаний на C ++, повинен взаємодіяти з кодом, написаним іншими мовами, оскільки немісцеві передачі управління можуть працювати безперебійно лише через модулі, якщо модулі знають, як координувати один одного.
supercat

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 ++". Напевно, було б точніше.
користувач673679

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

1

Як ви бачили в інших відповідях, ви платите, коли ви посилаєтесь на загальні бібліотеки та виклику складних конструкторів. Тут немає особливого питання, більше захоплення. Я зазначу деякі аспекти реального світу:

  1. Барн мав основний принцип дизайну, щоб ніколи не дозволяти ефективності бути причиною для перебування в C, а не C ++. Зважаючи на це, потрібно бути обережними, щоб досягти цієї ефективності, і є випадкові ефективність, яка завжди працювала, але не була "технічно" в специфікаціях C. Наприклад, макет бітових полів насправді не вказаний.

  2. Спробуйте переглядати ostream. О мій боже його роздутий! Я не був би здивований, коли знайшов там симулятор польоту. Навіть stdlib's printf () зазвичай працює близько 50K. Це не ліниві програмісти: половина розміру printf стосувалася непрямих аргументів точності, якими більшість людей ніколи не користується. Практично кожна бібліотека дійсно обмежених процесорів створює власний вихідний код замість printf.

  3. Збільшення розмірів, як правило, забезпечує більш обмежений та гнучкий досвід. Як аналогія, торговий автомат продасть чашку кавоподібної речовини за кілька монет, і вся транзакція займає менше хвилини. Заїзд у хороший ресторан передбачає сервірування столу, сидіння, замовлення, очікування, отримання гарної чашки, отримання рахунку, оплата за ваш вибір форм, додавання чайових напоїв та побажання гарного дня на виході. Це інший досвід і зручніше, якщо ви завітаєте з друзями за складною їжею.

  4. Люди все ще пишуть ANSI C, хоча рідко K&R C. Мій досвід - ми завжди компілюємо його за допомогою компілятора C ++, використовуючи кілька налаштувань конфігурації, щоб обмежити те, що перетягується. Є хороші аргументи для інших мов: Go видаляє поліморфні накладні та шалений препроцесор ; було кілька хороших аргументів щодо розумнішої упаковки поля та компонування пам’яті. ІМХО Я думаю, що будь-який мовний дизайн повинен починатися з переліку цілей, як і Дзен Пітона .

Це була весела дискусія. Ви запитуєте, чому ви не можете мати магічно малі, прості, елегантні, повні та гнучкі бібліотеки?

Відповіді немає. Відповіді не буде. Це відповідь.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.