Pull to refresh

Comments 32

Пример read1 & read2, мне только один аспект непонятен.
Почему вы возвращаете сам вектор, это же сопряжено с копированием целого вектора строк? Если вам не нравится, что в read2 будет создан вектор, который потом нужно удалять, почему бы не передавать в неё извне ссылку на вектор?
А компилятор не оптимизирует? NRVO и всё такое. А даже если нет, то для каких-то случаев, когда эта оптимизация ничего не ускорит (функция вызывается 1 раз при старте программы например) можно ради читабельности и так оставить.
Потому что должно отработать RVO.
Цитата из статьи на Википедии про конвенции вызова:
Если размер возвращаемого значения функции не больше размера регистра eax, возвращаемое значение сохраняется в регистре eax. Иначе, возвращаемое значение сохраняется на вершине стека, а указатель на вершину стека сохраняется в регистре eax.

Таким образом, ссылочная переменная создаётся неявно.
Благодарю, разобрался, только здесь не конвенция о вызовах, здесь немного другой механизм,
en.wikipedia.org/wiki/Copy_elision

To avoid this, an implementation may create a hidden object in the caller's stack frame, and pass the address of this object to the function.

Перевод
Чтобы избежать этого (т.е. копирования объектов типа класс ) может быть неявно создан объект требуемого типа в стековом кадре вызывающей функции, после чего адрес этого объекта передаётся в вызываемую функцию.
Dr_Dash прав по поводу копирования. Если точнее конструкция
vector b = read2(is);
будет компилироваться в следующий псевдокод

vector b;
vector stack_tmp_var; //переменная на стеке которая нужна для вызова read2
vector* pvec = read2(&stack_tmp_var, is) // pvec — это и есть eax
b = stack_tmp_var // или вероятнее в декомпиляторе будет vector.operator_assign(&b,pvec)

То-есть операция копирования все равно будет происходить. По крайней мере в debug версии приложения будет сделано так. Какой код будет в версии release мне сказать сложно — все зависит от уровня оптимизации. Но я часто при реверсинге кода сталкиваюсь с такой конструкцией кода.
Я поправлюсь. В моем предыдущем примере копирования объектов действительно не будет. Но в
vector b;
b = read2(is);
копирование объектов будет.
И здесь, также, не будет копирования. NRVO в функции сразу создаст внешний объект для результата, а b = read2(is); — вызовет перемещение, где, по сути, копируется только указатель.
Эмм, всегда был уверен, что вектор это просто указатель технически
Нет. Это не так.
Попробуйте написать
const int sz = sizeof(vector);
и вы получите от 16 до 40 байт. Зависит от компилятора и debug/release версий.
Класс std::vector не является COW объектом. При операторе копирования происходит копирования всего внутреннего буфера.
Попробовал на g++ на ubuntu/lts 64 битном, размер вектора 12 байт. Т.е. 1 указатель плюс 1 инт для быстрого хранения размера.
Это несколько странный результат так как размер в 64х тоже должен быть 8байтным. Если это 32ный компилятор то еще можно обьяснить указателем и 2мя размерами.
В visualstudio как правило 16 байт в x86. 2 указателя размер и резервед размер.
Однако вектор, кроме указателя как такового, это ещё и содержимое массива, и оно, если бы не NRVO тоже копировалось бы из вектора в вектор. А старый вектор пришлось бы ещё и удалять, каждый элемент.
Есть гипотеза, что кодогенерация…
Судя по пространству имен это ещё и не очень C++
“Если вы хотите улучшить качество кода в организации, замените все принципы кодинга одной целью: никаких сырых циклов!..
Дословно: если вы пишете сырой цикл, скорее всего вы просто не знаете алгоритмов STL"
это правило не работает когда критична скорость выполнения цикла и программы, алгоритмы STL хороши, но слишком универсальны и потому обычно работают в несколько раз медленнее, чем программы написанные под специализированный класс задач.
С другой стороны, когда скорость не критична, или циклы не велики и не велико число их повторов, то STL вполне достаточно.
Как говорится правила придуманы для того что бы их нарушать.
Со временем мощность вычислительных средств будет расти и STL сможет решать всё больший круг задач, включая критичные ко времени выполнения.
потому обычно работают в несколько раз медленнее
А потом включаем -O2, сравниваем, смотрим на свой «специализированный» код, идём за чашечкой кофе, сидим на стуле глядя в монитор и плачем.
это справедливо только в одном случае: std::sort может быть медленнее какой-нибудь пузырьковой сортировки на малом массиве из-за аллокации буфера. Все остальные STL алгоритмы, будучи оптимизированными, производят ровно такой же бинарный код, как и при ручном написании циклов. И даже в приведенном мной примере, лучше реализовать такую сортировку не в виде цикла, а в виде отдельной функции типа bubble_sort(RandomIt begin, RandomIt end)
Вероятно имелось в виду, что зная конкретный тип данных можно заменять циклы на специализированные функции. К примеру цикл копирования raw объектов можно ускорить на 2-3 порядка с помощью memcpy.
любой уважающий себя компилятор может заменить копирование pod данных на memcpy и сам
Со временем мощность вычислительных средств будет расти и STL сможет решать всё больший круг задач, включая критичные ко времени выполнения.
Да-да… а потом мы удивляемся: чтой-то у нас Ворд еле ворочается на машинке 10-летнего возраста… а ведь в своё время всё работало нормально.

Кстати, кроме старых машин есть ещё и новые, но специфические компьютеры. Одноплатники, например.
Есть некоторые сомнения в качестве исходной статьи. Автор взял список С++ Core Guidelines и, на мой взгляд неудачно, откомментировал.

Правило 1. Пример с циклом не показателен. Во-первых, первый пример абсолютно ясен, если вы сколько-нибудь долго пишете на C/C++. Во-вторых, время компиляции второго примера будет чуть больше (умножим на 100500 таких «циклов» в большом проекте). Кстати, какого типа будет sum во втором примере? — ага, double.

«Никаких сырых циклов» — вообще бред. Скорее всего цитата просто выдернута из контекста.

Правило 2. Результат работы функций read1 и read2 будут различны. Первая считывает в каждый массив char ровно по maxstring символов (за исключением последнего куска данных), включая пробельные символы и переводы строк. Концевой '\0' в массивы не добавляется. Вторая разбивает входные данные на строки по разделителям (пробельные символы и концы строк по умолчанию), которые в выходные данные не попадают.

Кроме того, в первой функции в параметре nread возвращается адрес локальной переменной. Это к слову о квалификации автора как программиста C++.

И «никаких сырых циклов»!

Правило 5. Спорно. Зависит как от конкретного кода, так и от размера экрана. И совершенно не факт, что определив новые логические сущности, разделив «на функции и объекты с методами», вы не получите двукратное увеличение объёма кода, разделённого на несколько файлов, с оверхедом в виде вызова функций и методов.

Кстати, это уже к автору перевода, термин «область видимости» имеет несколько другую семантику, если имеется ввиду язык программирования. Правило 5 лучше бы звучало как «пишите по возможности небольшие функции».

Правило 6. Оператор if (result.second) не скомпилируется в обоих случаях, поскольку имеет тип std::string, который неявно не приводится к bool.
Полностью согласен с вашим подходом, считаю что во всем должна быть мера и самое главное правило — код должен быть понятен, даже если его читает средней руки программист.

В случае Правила 1 предпочел бы такой код:
int sum = 0;

for (const auto& s : v)
    sum += s;

Плюсы:
1. В меру декларативный стиль. И компилятору хорошо и человеку.
2. При изменении без в цикл можно без проблем втянуть любой нужный контекст без плясок и страданий.

Дополнение к вашему замечанию к Правилу 2:
там вообще ничего не вернется, указатель просто локально переписывается, а значение не изменяется.
Во-вторых, время компиляции второго примера будет чуть больше (умножим на 100500 таких «циклов» в большом проекте).

В случае Правила 1 предпочел бы такой код:
for (const auto& s : v) ...

Плюсы:
1. В меру декларативный стиль. И компилятору хорошо и человеку.

Компилятор разворачивает range-based for в виде цикла с итераторами. Так что компилироваться такой вариант будет не быстрее, чем std::accumulate
И??? Не понял вашу мысль.
Моя мысль в том, что из того что компилятор разворачивает range-based for в виде цикла с итераторами, он будет выполняться не медленнее чем std::accumulate, т.к. кроме итераторов есть еще другие полезные действия.
Также, range-based for безопаснее, так как нет возможности перепутать или указать не те (несовместимые) итераторы.
Кроме того, в моей практике, мне это код еще приходится поддерживать — вводить некий контекст и дополнительную логику, а этот синтетический код с std::accumulate в таком случае становиться все более запутанным. Я к тому, что в реальном коде, когда, вдруг, появляется необходимость в дополнительном контексте, то в случае с std::accumulate, вам придется передавать лямбду с уродским синтаксисом. А зачем, если есть range-based for.

Вообще-то я писал про время компиляции, а не про скорость работы. А для того, что бы откомпилировать std::accumulate, компилятору придётся сделать некий объём дополнительной работы по анализу шаблонов и пр.

Да, в примере 1 правила 2 через nread ничего не вернётся. Поторопился. Я как увидел, что в аргумент функции пишется адрес локальной переменной, у меня забрало упало.


Учеников жалко. Не повезло с преподавателем.

Там не только не вернётся, там вообще ошибка
nread = &elemcount; //плохо
*nread = elemcount;// хорошо
Бле, сорян. Я когда постил, то чекал с оригиналом и автоматом оттуда ошибку утащил :(
С преподавателем-то всё нормуль, а вот с ретранслятором в виде меня — ниочинь, да. Не прочекал на ещё один заход код, а сверил его только с ориджином.
В правиле 1, я полагаю, основной посыл в том, чтобы придерживаться stl-стиля в работе с контейнерами и основной упор делается на то, как хотелось бы, чтобы это выглядело в современном коде. Утверждение, что тем, кто пишет давно все ясно, совершенно справедливо, но, полагаю, будущая книга ориентирована на поколение, для которых 14 или 17 полюсы это и есть норма, других версий нет :) Соответственно и подходы к работе с stl навязываются самой библиотекой.

Я сам не очень понял, к чему был комментарий о типе sum, не заметил какой-то подвох?

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

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

Правило 5 действительно можно трактовать как призыв писать небольшие функции, как частный случай. Речь в принципе идет о сокращении времени жизни объектов. Это касается циклов, условий и тел функций. Призыв не давать объектам жить дольше, чем это требует задача.

В 6 правиле кажется с примером все нормально, insert возвращает pair<iterator,bool>. result.second имеет тип bool.
Sign up to leave a comment.