Передача по ссылке или по значению? Передача параметров по ссылке и по значению Передача параметров по ссылке.

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

В языках программирования имеется два основных способа передачи параметров подпрограмме. Первый из них – передача по значению . При его применении в формальный параметр подпрограммы копируется значение фактического параметра (аргумента). В таком случае изменения формального параметра на фактический аргумент не влияют.

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

В языке С есть только один способ сопоставления фактических и формальных параметров – передача по значению (передачи параметров по ссылке есть в С++). В Паскале есть передача по значенияю и по ссылке. Бывают и другие методы (в Fortran – копирование-восстановление, в Algol – передача по имени).

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

Метод передачи по значению реализуется следующим способом:

    формальный параметр рассматривается как локальная переменная, так что память для нее выделяется в записи активации вызываемой функции, т.е. в стеке;

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

19.2. Передача параметров в функции в языке с

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

void f(int k) { k = -k; } void main() { int i = 1; f(i); printf("i = %d\n", i); // результат: i = 1 }

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

19.3. Передача указателей в функции

А что делать, если функция должна изменить значение фактического параметра? Самый очевидный, но не самый лучший, способ – заменить такой параметр глобальной переменной. Минус – повышение шансов ошибиться из-за неучтенных побочных эффектов при вызове функций.

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

Указатель передается функции так, как и любой другой аргумент – по значению. Понятно, что при передаче адреса параметр следует объявлять как один из типов указателей.

Поскольку функция получает копию аргумента, она не сможет повлиять на сам указатель. Но она может записать все, что угодно туда, куда он направлен, используя для обращения к значению аргумента-оригинала операцию разыменования *.

Задача. Написать функцию для замены местами значений двух переменных и вызвать ее из функции main ().

void swap(int *pa, int *pb) { // параметры-указатели

*pa = *pb; // занести b в a

*pb = temp; // занести a в b

void main(void) {

int i = 10, j = 20;

printf("i и j перед обменом значениями: %d %d\n", i, j);

swap(&i, &j); // передаем адреса переменных i и j

Функция swap() может выполнять обмен значениями двух переменных, на которые указывают pa и pb, потому что в функцию передаются адреса переменных, а не их значения. Внутри функции, используя стандартные операции с указателями, можно получить доступ к содержимому переменных и провести обмен их значений.

Обратить внимание! В любую функцию, в которой используются параметры в виде указателей, необходимо при вызове передавать адреса аргументов, используя операцию взятия адреса &.

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

void main(void) {

int i = 10, j = 20;

int *pi = &i, *pj = &j;

printf("i и j перед обменом значениями: %d %d\n", i, j);

swap(pi, pj); // передаем адреса переменных i и j

printf("i и j после обмена значениями: %d %d\n", i, j);

Здесь мы работаем с указателями как с обычными переменными – засылаем в них значения с помощью оператора присваивания, а потом передаем функции.

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

Задача. Написать две функции для вычисления суммы двух отрицательных чисел и их вызов из функции main (). Исходные данные должны вводиться в функции main (). Первая функция должна возвращать заданную величину. Во второй функции обеспечить контроль правильности исходных данных. Функция, кроме вычисления заданной величины, должна возвращать признак правильности исходных данных.

int sum1(int a, int b) {

int sum2(int a, int b, int *sum) {

if (a >= 0 || b >= 0)

return 0; // признак неверных данных

return 1; // признак правильных данных

void main(void) {

scanf(“%d %d”, &x, &y);

printf("Сумма 1 = %d\n", sum1(x,y));

if (sum2(x,y,&s) == 1)

printf("Сумма 2 = %d\n", s);

printf("Неверные данные!\n");

Когда я начинал программировать на C++ и усиленно штудировал книги и статьи, то неизменно натыкался на один и тот же совет: если нам нужно передать в функцию какой-то объект, который не должен изменяться в функции, то он всегда должен передаваться по ссылке на константу (ППСК), за исключением тех случаев, когда нам нужно передать либо примитивный тип, либо сходную с оными по размеру структуру. Т.к. за более чем 10 лет программирования на C++ я очень часто встречался с этим советом (да и сам его давал неоднократно), он давно «впитался» в меня - я на автомате передаю все аргументы по ссылке на константу. Но время идёт и уже прошло 7 лет, как мы имеем в своём распоряжении C++11 с его семантикой перемещения, в связи с которой я всё больше и больше слышу голосов, подвергающих старую добрую догму сомнениям. Многие начинают утверждать, что передача по ссылке на константу это прошлый век и теперь нужно передавать по значению (ППЗ). Что стоит за этими разговорами, а также какие выводы мы можем из этого всего сделать, я и хочу обсудить в этой статье.

Книжная мудрость

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

Мэйерс

Хорошо, вот мы имеем класс, в котором все параметры передаются по ссылке, есть ли с этим классом какие-то проблемы? К сожалению, есть, и эта проблема лежит на поверхности. У нас в классе функционально 2 сущности: первая принимает значение на этапе создания объекта, а вторая позволяет изменить ранее установленное значение. Сущности-то у нас две, а вот функции четыре. А теперь представьте, что у нас может быть не 2 подобных сущности, а 3, 5, 6, что тогда? Тогда нас ждёт сильное раздувание кода. Поэтому, чтобы не плодить массы функций, появилось предложение отказаться от ссылок в параметрах вообще:

Template class Holder { public: explicit Holder(T value): m_Value{move(value)} { } void setValue(T value) { m_Value = move(value); } const T& value() const noexcept { return m_Value; } private: T m_Value; };

Первое преимущество, которое сразу бросается в глаза, заключается в том, что кода стало значительно меньше. Его даже меньше чем в самом первом варианте, за счёт удаления const и & (правда, добавили move ). Но ведь нас всегда учили, что передача по ссылке производительнее, чем передача по значению! Так оно было до C++11, так оно и есть до сих пор, но теперь, если мы посмотрим на этот код, то увидим, что копирования здесь не больше чем в первом варианте, при условии, что у T есть конструктор перемещения . Т.е. сама по себе ППСК была и будет быстрее ППЗ, но ведь код как-то использует переданную ссылку, и зачастую этот аргумент копируется.

Однако, это не вся история. В отличии от первого варианта, где у нас есть только копирование, тут добавляется ещё и перемещение. Но ведь перемещение это дешёвая операция, правда? На эту тему, у рассматриваемой нами книги Мэйерса, тоже есть глава («Item 29»), которая озаглавлена так: «Assume that move operations are not present, not cheap and not used». Основная мысль должна быть ясна из названия, но если хочется подробностей, то всенепременно ознакомьтесь - я на этом останавливаться не буду.

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

Видит он его в том, что в случае передачи rvalue, т.е. какого-то такого вызова: Holder holder{string{"me"}}; , вариант с ППСК даст нам копирование, а вариант с ППЗ даст нам перемещение. С другой стороны, если передача будет такой: Holder holder{someLvalue}; , то ППЗ однозначно проигрывает за счёт того, что он выполнит и копирование, и перемещение, тогда как в варианте с ППСК будет только одно копирование. Т.е. получается, что ППЗ, если рассматривать сугубо эффективность, это некоторый компромисс между количеством кода и «полноценной» (через && ) поддержкой семантики перемещения.

Именно поэтому Скотт так тщательно сформулировал свой совет и так осторожно его продвигает. Мне даже показалось, что он приводит его нехотя, как бы под давлением: он не мог не разместить рассуждения на эту тему в книге, т.к. она довольно широко обсуждалась, а Скотт всегда был сборщиком коллективного опыта. Кроме того, уж очень мало доводов он приводит в защиту ППЗ, а вот тех, что ставят эту «технику» под сомнение, он приводит немало. Мы ещё рассмотрим его доводы «против» в последующих разделах, здесь же мы кратко повторим аргумент, который Скотт приводит в защиту ППЗ (мысленно добавляем «если объект поддерживает перемещение и оно дёшево» ): позволяет избежать копирования при передаче rvalue-выражения в качестве аргумента функции. Но хватит мучить книгу Мэйерса, давайте уже перейдём к другой книге.

Кстати, если кто-то читал книгу и удивляется, что я не привожу здесь вариант с тем, что Мэйерс называл универсальными ссылками (universal references) - теперь они известны как пробрасывающие ссылки (forwarding references), - то это легко объясняется. Я рассматриваю только ППЗ и ППСК, т.к. вводить шаблонные функции для методов, которые шаблонами не являются, только ради того, чтобы поддержать передачу по ссылкам обоих типов (rvalue/lvalue) считаю дурным тоном. Не говоря уже о том, что код получается другим (больше нет константности) и несёт с собой другие проблемы.

Джосаттис и компания

Последней книгой мы рассмотрим «C++ Templates» , она же является наиболее свежей из всех упомянутых в этой статье книг. Вышла она под конец 2017 года (а внутри книги вообще 2018 указан). В отличии от других книг, эта целиком посвящена шаблонам, а не советам (как у Мэйерса) или C++ в целом, как у Страуструпа. Поэтому и плюсы/минусы тут рассматриваются с точки зрения написания шаблонов.

Данной теме посвящена целая глава 7, которая имеет красноречивое название «By value or by reference?». В этой главе авторы довольно кратко, но ёмко описывают все методы передачи со всеми их плюсами и минусами. Анализ эффективности здесь практичеки не приводится, и как должное принимается то, что ППСК будет быстрее ППЗ. Но при всём при этом в конце главы авторы рекомендуют использовать ППЗ для шаблонных функций по умолчанию. Почему? Потому что используя ссылку, шаблонные параметры выводятся полностью, а без ссылки «разлагаются» (decay), что благоприятно сказывается на обработке массивов и строковых литералов. Авторы считают, что если уж для какого-то типа ППЗ окажется неэффективным, то всегда можно использовать std::ref и std::cref . Такой себе совет, честно говоря, много вы видели желающих использовать вышеозначенные функции?

Что же они советуют касательно ППСК? Они советуют использовать ППСК тогда, когда производительность критична или есть другие весомые причины не использовать ППЗ. Конечно, мы здесь говорим только о шаблонном коде, но этот совет прямо противоречит всему, чему учили программистов на протяжении десятка лет. Это не просто совет рассмотреть ППЗ как альтернативу - нет, это совет альтернативой сделать ППСК.

На этом завершим наш книжный тур, т.к. мне не известны другие книги, с которыми нам стоило бы ознакомиться по данному вопросу. Перейдём в другое медиапространство.

Сетевая мудрость

Т.к. живём в век интернета, то на одну книжную мудрость полагаться не стоит. Тем более, что многие авторы, которые раньше писали книги, теперь просто пишут блоги, а от книг отказались. Одним из таких авторов является Герб Саттер, который в мае 2013 года опубликовал в своём блоге статью «GotW #4 Solution: Class Mechanics» , которая хоть и не является целиком посвящённой освещаемой нами проблеме, всё-таки задевает её.

Итак, в первоначальном варианте статьи Саттер просто повторил старую мудрость: «передавайте параметры по ссылке на константу», но этого варианта статьи мы уже не увидим, т.к. в статье находится обратный совет: «если параметр всё равно будет скопирован, тогда передавайте его по значению». Опять пресловутое «если». Почему Саттер изменил статью, и откуда я об этом узнал? Из комментариев. Почитайте комментарии к его статье, они, кстати, интереснее и полезнее самой статьи. Правда, уже после написания статьи, Саттер всё-таки поменял своё мнение и такого совета он больше не даёт. Изменившееся мнение можно обнаружить в его выступлении на CppCon в 2014 году: «Back to the Basics! Essentials of Modern C++ Style» . Посмотрите обязательно, мы же перейдём к следующей интернет-ссылке.

А на очереди у нас главный программистский ресурс 21 века: StackOverflow. А точнее ответ, с количеством положительных реакций превышающим 1700 на момент написания этой статьи. Вопрос звучит так: What is the copy-and-swap idiom? , и, как должно быть понятно из названия, не совсем по теме, что мы рассматриваем. Но в своём ответе на этой вопрос, автор затрагивает и интересующую нас тему. Он тоже советует использовать ППЗ «если аргумент всё равно будет скопирован» (пора уже и на это аббревиатуру вводить, ей Богу). И в целом этот совет выглядит вполне уместным, в рамках его ответа и обсуждаемого там operator= , но автор берёт на себя смелость давать подобный совет в более широком ключе, а не только в этом частном случае. Более того, он идёт дальше всех рассмотренных нами ранее советов и призывает делать это даже в C++03 коде! Что же подвигло автора на подобные умозаключения?

Судя по всему, основное вдохновение автор ответа черпал из статьи ещё одного книжного автора и по совместительству разработчика Boost.MPL - Дэйва Абрахамса. Статья называется «Want Speed? Pass by Value.» , и была она опубликована ещё в августе 2009 года, т.е. за 2 года до принятия C++11 и введения семантики перемещения. Как и в предыдущих случаях, рекомендую читателю самостоятельно ознакомиться со статьей, я же приведу основные доводы (довод, в сущности, один), которые Дэйв приводит в пользу ППЗ: нужно использовать ППЗ, потому что с ним хорошо работает оптимизация «пропуск копирования» (copy elision), которая отсутствует при ППСК. Если почитать комментарии к статье, то можно увидеть, что продвигаемым им совет не является универсальным, что подтверждает сам автор, отвечая на критику комментаторов. Тем не менее статья содержит явный совет (guideline) использовать ППЗ, если аргумент всё равно будет скопирован. Кстати, кому интересно, можете почитать статью «Want speed? Don’t (always) pass by value.» . Как должно быть ясно из названия, это статья является ответом на статью Дэйва, так что если прочли первую, то и эту прочтите обязательно!

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

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

Первый набор правил, который я хочу рассмотреть, явился последней каплей, заставившей меня всё-таки взяться за эту статью. Этот набор является частью утилиты clang-tidy и вне её не существует. Как и всё, что связано с clang, эта утилита весьма популярна и уже получила интеграцию с CLion и Resharper C++ (именно так я с ней и столкнулся). Итак, clang-tydy содержит правило modernize-pass-by-value , которое срабатывает на конструкторах, принимающих аргументы посредством ППСК. Это правило предлагает нам заменить ППСК на ППЗ. Более того, на момент написания статьи в описании данного правила содержится ремарка, что это правило пока работает только для конструкторов, но они (кто они?) с удовольствием примут помощь от тех, кто распространит это правило на другие сущности. Там же, в описании, есть и ссылка на статью Дэйва - понятно откуда ноги растут.

Наконец, в завершении рассмотрения чужой мудрости и авторитетных мнений, предлагаю посмотреть на официальные рекомендации по написанию C++ кода: C++ Core Guidelines , основными редакторами которых являются Герб Саттер и Бъярн Страуструп (неплохо, правда?). Так вот, эти рекомендации содержат следующее правило: «For “in” parameters, pass cheaply-copied types by value and others by reference to const» , которое полностью повторяет старую мудрость: ППСК везде и ППЗ для небольших объектов. В описании этого совета приводятся несколько альтернатив, которые предлагается рассмотреть в случае если передача аргументов нуждается в оптимизации . Но в списке альтернатив ППЗ не представлена!

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

Анализ

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

А есть ли преимущество у ППЗ?

Итак, прежде чем рассматривать аргументы «за» и «против», я предлагаю посмотреть какое и в каких случаях преимущество нам даёт передача по значению. Пусть у нас есть какой-то такой класс:

Class CopyMover { public: void setByValuer(Accounter byValuer) { m_ByValuer = std::move(byValuer); } void setByRefer(const Accounter& byRefer) { m_ByRefer = byRefer; } void setByValuerAndNotMover(Accounter byValuerAndNotMover) { m_ByValuerAndNotMover = byValuerAndNotMover; } void setRvaluer(Accounter&& rvaluer) { m_Rvaluer = std::move(rvaluer); } };

Хотя в рамках данной статьи нам интересны только первые д��е функции, я привел четыре варианта, чтобы просто использовать их в качестве контраста.

Класс Accounter - это простой класс, который считает сколько раз он был скопирован/перемещён. А в классе CopyMover у нас реализованы функции, которые позволяют рассмотреть следующие варианты:

    перемещением переданного аргумента.

    Передача по значению, с последующим копированием переданного аргумента.

Теперь, если мы передадим lvalue в каждую из этих функций, к примеру вот так:

Accounter byRefer; Accounter byValuer; Accounter byValuerAndNotMover; CopyMover copyMover; copyMover.setByRefer(byRefer); copyMover.setByValuer(byValuer); copyMover.setByValuerAndNotMover(byValuerAndNotMover);

то получим следующие результаты:

Очевидным победителем является ППСК, т.к. даёт всего одно копирование, тогда как ППЗ даёт одно копирование и одно перемещение.

Теперь попробуем передать rvalue:

CopyMover copyMover; copyMover.setByRefer(Accounter{}); copyMover.setByValuer(Accounter{}); copyMover.setByValuerAndNotMover(Accounter{}); copyMover.setRvaluer(Accounter{});

Получим следующее:

Тут уже однозначного победителя нет, т.к. и у ППЗ, и у ППСК по одной операции, но в силу того, что ППЗ использует перемещение, а ППСК - копирование, можно отдать победу ППЗ.

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

Void setByValuer(Accounter byValuer, CopyMover& copyMover) { copyMover.setByValuer(std::move(byValuer)); } void setByRefer(const Accounter& byRefer, CopyMover& copyMover) { copyMover.setByRefer(byRefer); } ...

Использовать их мы будем точно так же, как делали без них, поэтому повторять код не стану (посмотрите в хранилище, если нужно). Итак, для lvalue результаты будут такими:

Заметьте, что ППСК увеличивает разрыв с ППЗ, оставаясь с единственной копией, тогда как у ППЗ уже целых 3 операции (на одно перемещение больше)!

Теперь передаём rvalue и получаем такие результаты:

Теперь ППЗ имеет 2 перемещения, а ППСК всё то же одно копирование. Можно ли теперь выдвинуть ППЗ в победители? Нет, т.к. если одно перемещение должно быть как минимум не хуже, чем одно копирование, про 2 перемещения мы подобного сказать уже не можем. Поэтому победителя в этом примере не будет.

Мне могут возразить: «Автор, у Вас предвзятое мнение и Вы притягиваете за уши то, что Вам выгодно. Даже 2 перемещения будут дешевле чем копирование!». Я не могу согласиться с подобным утверждением в общем , т.к. то, насколько перемещение быстрее копирования зависит от конкретного класса, но мы ещё рассмотрим «дешёвое» перемещение в отдельном разделе.

Тут мы затронули интересную вещь: мы добавили один косвенный вызов, и ППЗ прибавило в «весе» ровно на одну операцию. Думаю, что не нужно иметь диплом МГТУ для понимания того, что чем больше косвенных вызовов мы имеем, тем больше операций будет выполнено при использовании ППЗ, тогда как для ППСК количество будет оставаться неизменным.

Всё рассмотренное выше вряд ли стало для кого-то откровением, мы могли даже не проводить экспериментов - всё эти числа должны быть очевидны большинству C++ программистов с первого взгляда. Правда, один момент всё же заслуживает пояснения: почему в случае с rvalue у ППЗ нет копирования (или ещё одного перемещения), а есть только одно перемещение.

Что ж, мы рассмотрели разницу в передаче между ППЗ и ППСК, воочию понаблюдав за количеством копий и перемещений. Хотя очевидно, что преимущество ППЗ над ППСК даже в таких простых примерах мягко говоря не очевидно, я всё же, немного кривя душой, сделаю следующий вывод: если мы всё равно будем копировать аргумент функции, то имеет смысл рассмотреть передачу аргумента в функцию по значению. Зачем я сделал этот вывод? Чтобы плавно перейти к следующему разделу.

Если копируем...

Итак, мы добрались до пресловутого «если». Большинство встреченных нами аргументов не призывали повсеместно внедрять ППЗ вместо ППСК, они лишь призывали делать это «если всё равно аргумент будет скопирован». Пришло время разобраться, что не так с этим аргументом.

Хочу начать с небольшого описания того, как я пишу код. В последнее время мой процесс написания кода всё больше походит на TDD, т.е. написание любого метода класса начинается с написания теста, в котором этот метод фигурирует. Соответственно, начиная писать тест, и создавая после написания теста метод, я ещё не знаю буду ли я копировать аргумент. Конечно, не все функции создаются таким образом, часто ещё в процессе написания теста ты совершенно точно знаешь, что там будет за реализация. Но так происходит не всегда!

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

Получается, что мы модифицируем (либо же вообще планируем) интерфейс исходя из того, как он будет реализован. Не считаю себя экспертом по ООП и прочим теоретическим выкладкам архитектуры ПО, но подобные действия явно противоречат базовым правилам, когда реализация не должна влиять на интерфейс. Конечно, определённые детали реализации (будь то особенности языка или целевой платформы) так или иначе всё равно просачиваются через интерфейс, но нужно стараться сокращать, а не увеличивать количество таких вещей.

Ну да Бог с ним, пусть мы пошли по этому пути и всё-таки меняем интерфейсы в зависимости от того, что мы делаем в реализации в части копирования аргумента. Предположим мы написали такой метод:

Void setName(Name name) { m_Name = move(name); }

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

Void setName(Name name) { m_Name = move(name); emit nameChanged(m_Name); }

Есть ли в этом коде проблема? Есть. На каждый вызов setName мы посылаем сигнал, так что сигнал будет послан даже тогда, когда значение m_Name не изменилось. Помимо вопросов производительности, такая ситуация может привести к бесконечному циклу из-за того, что код, который получает вышеозначенное уведомление, каким-то образом приходит к тому, чтобы вызвать setName . Чтобы избежать всех этих проблем подобные методы чаще всего выглядят примерно так:

Void setName(Name name) { if(name == m_Name) return; m_Name = move(name); emit nameChanged(m_Name); }

Мы избавились от вышеописанных проблем, но теперь наше правило «если всё равно копируем...» дало сбой - больше нет безусловного копирования аргумента, теперь мы его копируем только при условии изменения! И что нам теперь делать? Менять интерфейс? Хорошо, давайте изменим интерфейс класса из-за этого исправления. А что если наш класс унаследовал этот метод из некоторого абстрактного интерфейса? Поменяем и там! Не много ли изменений из-за того, что изменилась реализация?

Опять мне могут возразить, мол автор, ты чего тут на спичках экономить вздумал, когда там это условие отработает? Да большую часть вызовов оно будет ложным! А в этом есть уверенность? Откуда? И если я решил экономить на спичках, так разве сам факт того, что мы использовали ППЗ не явился ли следствием именно такой экономии? Я лишь продолжаю «линию партии», ратующую за эффективность.

Конструкторы

Кратко пройдёмся и по конструкторам, тем более что для них есть специальное правило в clang-tidy, которое для других методов/функции пока не работает. Предположим, у нас есть такой класс:

Class JustClass { public: JustClass(const string& justString): m_JustString{justString} { } private: string m_JustString; };

Очевидно, что параметр копируется, и clang-tidy нам сообщит, что было бы неплохо переписать конструктор на такой:

JustClass(string justString): m_JustString{move(justString)} { }

И мне, честно говоря, сложно тут возразить - ведь и правда всегда копируем. И чаще всего, когда мы передаём что-либо через конструктор, мы это что-то копируем. Но чаще не значит всегда. Вот вам другой пример:

Class TimeSpan { public: TimeSpan(DateTime start, DateTime end) { if(start > end) throw InvalidTimeSpan{}; m_Start = move(start); m_End = move(end); } private: DateTime m_Start; DateTime m_End; };

Здесь мы копируем не всегда, а только тогда, когда даты представлены корректно. Конечно, в подавляющем большинстве случаев так и будет. Но не всегда .

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

Что можно вынести из этого раздела? То, что аргумент «если всё равно копируем...» является весьма спорным, т.к. далеко не всегда мы знаем, что копировать будем, а даже когда знаем, мы очень часто не уверены в том, что это так и будет продолжаться в дальнейшем.

Перемещение дёшево

С самого момента появления семантики перемещения, она начала оказывать серьёзное влияние на то, как пишется современный C++-код, и за прошедшее время это влияние только усилилось: оно и немудрено, ведь перемещение так дёшево по сравнению с копированием. Но так ли это? Правда ли, что перемещение это всегда дешёвая операция? Вот с этим мы и попытаемся разобраться в этом разделе.

Большой двоичный объект

Начнём с банального примера, пусть у нас есть такой класс:

Struct Blob { std::array data; };

Обычный большой двоичный объект (БДО, англ. BLOB), который может применяться в самых разных ситуациях. Давайте рассмотрим, что же нам будет стоить передача по ссылке и по значению. Использоваться наш БДО будет примерно так:

Void Storage::setBlobByRef(const Blob& blob) { m_Blob = blob; } void Storage::setBlobByVal(Blob blob) { m_Blob = move(blob); }

А вызывать эти функции будем так:

Const Blob blob{}; Storage storage; storage.setBlobByRef(blob); storage.setBlobByVal(blob);

Код для других примеров будет идентичен этому, только с другими именами и типами, поэтому приводить для оставшихся примеров я его не стану - всё есть в хранилище.

Прежде чем перейдём к измерениям, давайте попробуем предсказать результат. Итак, у нас есть std::array размером в 4 Кб, который мы хотим сохранить в объекте класса Storage . Как мы выяснили ранее, для ППСК у нас будет одно копирование, тогда как для ППЗ будет одно копирование и одно перемещение. Исходя из того, что array переместить невозможно, для ППЗ будет 2 копирования, против одного для ППСК. Т.е. мы вправе ожидать двукратного превосходства в производительности для ППСК.

Теперь давайте взглянем на результаты тестирования:

Этот и все последующие тесты выполнялись на одной машине с использованием MSVS 2017 (15.7.2) и с флагом /O2 .

Практика совпала с предположением - передача по значению получается в 2 раза дороже, потому что для array перемещение полностью эквивалентно копированию.

Строка

Рассмотрим другой пример, обычную строку std::string . Что мы можем ожидать? Мы знаем (я рассматривал это в статье ), что современные реализации различают string двух типов: короткие (в районе 16 символов) и длинные (те, что больше коротких). Для коротких используется внутренний буфер, который представляет собой обычный C-массив из char , а вот длинные уже будут размещаться в куче. Короткие строки нас не интересуют, т.к. результат там будет тот же, что и с БДО, поэтому сосредоточимся на длинных строках.

Итак, имея длинную строку, очевидно, что её перемещение должно быть довольно дёшево (просто переместить указатель), поэтому можно рассчитывать на то, что перемещение строки не должно вообще никак сказаться на результатах, и ППЗ должна дать результат не хуже ППСК. Проверим на практике и получим следующие результаты:

Мы же перейдём к объяснению этого «феномена». Итак, что происходит когда мы копируем существующую строку в уже существующую строку? Давайте рассмотрим банальный пример:

String first{64, "C"}; string second{64, "N"}; //... second = first;

У нас две строки размером в 64 символа, поэтому при их создании внутреннего буфера недостаточно, в результате обе строки размещаются в куче. Теперь мы копируем first в second . Т.к. размеры строк у нас одинаковые, очевидно, что в second выделено достаточно места, чтобы вместить все данные из first , поэтому second = first; будет представлять собой банальный memcpy , не более того. Но если мы рассмотрим слега изменённый пример:

String first{64, "C"}; string second = first;

то здесь уже не будет вызова operator= , но будет вызван конструктор копирования. Т.к. мы имеем дело с конструктором, то существующей памяти в нём нет. Её сначала надо выделить и только потом скопировать first . Т.е. это выделение памяти, а потом memcpy . Как мы с вами знаем, выделение памяти в глобальной куче это, как правило, дорогая операция, поэтому копирование из второго примера будет дороже копирования из первого. Дороже на одно выделение памяти в куче.

Какое это отношение имеет к нашей теме? Самое прямое, ведь первый пример показывает ровно то, что происходит при ППСК, а второй то, что происходит при ППЗ: для ППЗ всегда создаётся новая строка, тогда как для ППСК происходит переиспользование существующей. Разницу во времени выполнения вы уже видели, так что тут добавить нечего.

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

Мне, безусловно, могут возразить, что, мол, string стоит особняком, и большинство типов так не работают. На что я могу ответить следующее: всё, что описано ранее будет справедливо для любого контейнера, который выделяет память в куче сразу под пачку элементов. Кроме того, кто знает какие другие контекстно-зависимые оптимизации применяются в других типах?

Что стоит почерпнуть из этого раздела? То, что даже если перемещение действительно дёшево, не означает того, что замещение копирования на копирование+перемещение всегда будет давать результат сравнимый по производительности.

Сложный тип

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

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

Итак, поехали: Person с 10-ю полями типа string , который мы передаём с помощью ППСК и ППЗ в Storage :

Как вы можете видеть, мы имеем колоссальную разницу в производительности, что после предыдущих разделов не должно было стать для читателей неожиданностью. Также я считаю, что класс Person является достаточно «реальным», чтобы не отметать подобные результаты как абстрактные.

Кстати, когда я готовил эту статью, я подготовил ещё один пример: класс, который использует несколько объектов std::function . По моей задумке он тоже должен был показать преимущество в производительности ППСК над ППЗ, но получилось ровно наоборот! Но я не привожу этот пример здесь не потому, что мне не понравились результаты, а потому, что у меня не нашлось времени разобраться почему же такие результаты получаются. Тем не менее код в хранилище есть (Printers ), тесты - тоже, если у кого-то есть желание разобраться, я был бы рад услышать о результатах исследования. Я же планирую вернуться к этому примеру позже, и если до меня никто этих результатов не опубликует, то я рассмотрю их в отдельной статье.

Итоги

Итак, мы рассмотрели различные плюсы и минусы передачи по значению и по ссылке на константу. Рассмотрели некоторые примеры и посмотрели на производительность обоих методов в этих примерах. Разумеется, эта статья не может и не является исчерпывающей, но, на мой взгляд, в ней достаточно информации, чтобы принять самостоятельное и взвешенное решение по тому, какой же способ лучше использовать. Кто-то может возразить: «зачем использовать один способ, давайте отталкиваться от задачи!». Хотя я согласен с этим тезисом в общем виде, я не согласен с ним в данной ситуации. Я считаю, что в языке может быть только один способ передачи аргументов, который используется по умолчанию .

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

Многие программисты (C, C++ и прочие) с недоверием и страхом относятся к тому C++, который стал проявляться после 2011 года. Я слышал немало критики, что язык становится сложнее, что писать на нём теперь могут только «гуру» и т.п. Лично я считаю, что это не так - комитет наоборот много времени уделяет тому, чтобы язык стал дружелюбнее к новичкам и чтобы программистам меньше нужно было думать над особенностями языка. Ведь если нам не нужно бороться с языком, то остаётся время подумать над задачей. К этим упрощениями я отношу и умные указатели, и лямбда-функции и многое другое, что появилось в языке. При этом я не отрицаю того факта, что изучать теперь нужно больше, но что плохого в учении? Или в других популярных языках не происходит изменений, которые нужно изучать?

Дальше, я не сомневаюсь, что найдутся снобы, которые могут сказать в ответ: «Думать не хочется? Иди тогда на PHP пиши». Таким людям я даже отвечать не хочу. Приведу лишь пример из игровой действительности: в первой части Starcraft, когда новый рабочий создаётся в здании, то чтобы он начал добывать минералы (или газ), нужно было вручную его туда послать. Более того, у каждой пачки минералов был лимит, при достижении которого наращивание рабочих было бесполезным, и они даже могли мешать друг другу, ухудшая добычу. В Starcraft 2 это изменили: рабочие автоматически начинают добывать минералы (или газ), а также указывается сколько рабочих сейчас добывают и сколько лимит этого месторождения. Это очень сильно упростило взаимодействие игрока с базой, позволив ему сосредоточиться на более важных аспектах игры: построение базы, накопления войск и уничтожение противника. Казалось бы, это просто отличное нововведение, но что началось в сети! Люди (кто они?) начали визжать, что игра «оказуаливается» и «они убили Starcraft». Очевидно, что такие сообщения могли исходить только от «хранителей тайного знания» и «адептов высокого APM», которым нравилось находиться в неком «элитном» клубе.

Так вот, возвращаясь к нашей теме, чем меньше мне нужно думать над тем, как мне писать код, тем больше мне остаётся времени на то, чтобы думать над решением непосредственной задачи. Думать над тем, какой метод мне использовать - ППСК или ППЗ - ни на йоту не приближает меня к решению задачи, поэтому думать над такими вещами я просто отказываюсь и выбираю один вариант: передача по ссылке на константу. Почему? Потому что я не вижу никаких преимуществ у ППЗ в общих случаях, а частные случаи нужно рассматривать отдельно.

Частный случай, он на то и частный, что заметив то, что в каком-то методе ППСК оказывается узким местом, и, изменив передачу на ППЗ, мы получим важный прирост в производительности, я не задумываюсь применю ППЗ. Но по умолчанию я буду применять ППСК как в обычных функциях, так и в конструкторах. И по возможности буду пропагандировать именно этот способ везде, где только можно. Почему? Потому что считаю практику пропаганды ППЗ порочной из-за того, что львиная доля программистов не слишком сведущи (либо в принципе, либо ещё просто не вошли в курс дела), и они просто следуют советам. Плюс, если есть несколько противоречащих друг друг советов, то они выбирают тот, что попроще, а это приводит к тому, что в коде появляется пессимизация просто потому, что кто-то где-то что-то слышал. Ах да, ещё этот кто-то может привести ссылку на статью Абрахамса, чтобы доказать, что он прав. А ты потом сидишь, читаешь код и думаешь: а вот то, что здесь параметр передаётся по значению, это потому что программист, который это писал, пришёл с Java, просто начитался «умных» статей или тут действительно нужно ППЗ?

ППСК читается куда проще: человек явно знает «хороший тон» C++ и мы идём дальше - взгляд не задерживается. Практика применения ППСК преподавалась программистам C++ годами, какая такая причина от неё отказываться? Это приводит меня к ещё одному выводу: если в интерфейсе метода используется ППЗ, значит там же должен находиться комментарий, почему это именно так. В остальных случаях должна применяться ППСК. Разумеется, есть типы-исключения, но я об этом не упоминаю здесь просто потому, что это подразумевается: string_view , initializer_list , различные итераторы и т.п. Но это исключения, список которых может расширяться в зависимости от того, какие типы используются в проекте. Но суть остаётся неизменной со времён C++98: по умолчанию мы всегда применяем ППСК.

Для std::string разницы на маленьких строках скорее всего не будет, мы поговорим об этом позже.

Параметры в функцию могут передаваться одним из следующих способов:

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

int GetMax(int, int);

принимает два целочисленных аргумента по значению.

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

С помощью указателей.

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

double Glue(long& var1, int& var2);

получает две ссылки на переменные типа long и int. При передаче в функцию параметра-ссылки компилятор автоматически передает в функцию адрес переменной, указанной в качестве аргумента. Ставить знак амперсанда перед аргументом в вызове функции не нужно. Например, для предыдущей функции вызов с передачей параметров по ссылке выглядит следующим образом:

Glue(var1, var2);

Пример прототипа функции при передаче параметров через указатель приведен ниже:

void SetNumber(int*, long*);

Кроме того, функции могут возвращать не только значение некоторой переменной, но и указатель или ссылку на него. Например, функции, прототип которых:

*int Count(int); &int Increase();

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

Эффективность передачи адреса объекта вместо самой переменной ощутима и в скорости работы, особенно, если используются большие объекты, в частности массивы (будут рассмотрены позже).

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

const int* FName(int* const Number)

принимает и возвращает указатель на константный объект типа int. Любая попытка модифицировать такой объект в пределах тела вызываемой функции вызовет сообщение компилятора об ошибке. Рассмотрим пример, иллюстрирующий использование константных указателей.

#include

int* const call(int* const);

int X = 13; int* pX = &X; call(pX);

int* const call(int* const x)

//*x++; II нельзя модифицировать объект! return x;

Вместо приведенного выше синтаксиса константного указателя в качестве альтернативы при передаче параметров можно использовать константные ссылки, например:

const int& FName (const int& Number)

имеющие тот же смысл, что и константные указатели.

#include

const int& call(const int& x)

// нельзя модифицировать объект!

Заранее извиняюсь за пафосную аннотацию про "расстановку точек", но надо же как-то завлечь вас в статью)) Со своей стороны постараюсь, чтобы аннотация все же оправдывала ваши ожидания.

Вкратце о чем речь

Все это и так знают, но все же в начале напомню, как в 1С могут передаваться параметры метода. Передаваться они могут "по ссылке" и "по значению". В первом случае, мы передаем в метод то же самое значение, что и в точке вызова, а во втором - его копию.

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

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

Процедура ПоЗначению(Знач Параметр) Параметр = 2; КонецПроцедуры Параметр = 1; ПоЗначению(Параметр); Сообщить(Параметр); // выведет 1

Все работает, как обещано - изменение (а правильнее сказать "замена") значения параметра не приводит к изменению значения вне метода.

Ну а в чем прикол-то?

Интересные моменты начинаются, когда мы начинаем передавать в качестве параметров не примитивные типы (строки, числа, даты и т.п.), а объекты. Вот тут-то и всплывают такие понятия, как "мелкая" и "глубокая" копия объекта, а также указатели (не в терминах C++, а как абстрактные дескрипторы (handles)).

При передаче объекта (например, ТаблицыЗначений) по ссылке, мы передаем само значение указателя (некий handle), который в памяти платформы "держит" объект. При передаче по значению платформа сделает копию этого указателя.

Иными словами, если, передавая объект по ссылке, в методе мы присвоим параметру значение "Массив", то в точке вызова получим массив. Повторное присваивание значения, переданного по ссылке, видно из места вызова.

Процедура ОбработатьЗначение(Параметр) Параметр = Новый Массив; КонецПроцедуры Таблица = Новый ТаблицаЗначений; ОбработатьЗначение(Таблица); Сообщить(ТипЗнч(Таблица)); // выведет Массив

Если же, мы передадим объект по значению, то в точке вызова наша ТаблицаЗначений не пропадет.

Содержимое объекта и его состояние

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

Процедура ОбработатьЗначение(Параметр) Параметр.Очистить(); КонецПроцедуры Таблица = Новый ТаблицаЗначений; Таблица.Добавить(); ОбработатьЗначение(Таблица); Сообщить(Таблица.Количество()); // выведет 0

При передаче объектов в методы платформа оперирует указателями (условными, не прямыми аналогами из C++). Если объект передается по ссылке, то ячейка памяти виртуальной машины 1С, в которой лежит данный объект, может быть перезаписана другим объектом. Если объект передается по значению, то указатель копируется и перезапись объекта не приводит к перезаписи ячейки памяти с исходным объектом.

В то же время любое изменение состояния объекта (очистка, добавление свойств и т.п.) изменяет сам объект, и вообще никак не связано с тем, как и куда объект передавался. Изменилось состояние экземпляра объекта, на него может быть куча "по-ссылок" и "по-значений", но экземпляр всегда один и тот же. Передавая объект в метод, мы не создаем копию всего объекта.

И это верно всегда, за исключением...

Клиент-серверное взаимодействие

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

Как известно, не все объекты платформы являются сериализуемыми. Именно отсюда растет ограничение, что не все объекты можно передать в серверный метод с клиента. Если передать несериализуемый объект, то платформа начнет ругаться нехорошими словами.

  • Явное объявление намерений программиста. Глядя на сигнатуру метода, можно четко сказать, какие параметры входные, а какие выходные. Такой код легче читать и сопровождать
  • Для того, чтобы изменение на сервере параметра "по ссылке" было видно в точке вызова на клиенте, п араметры, передаваемые на сервер по ссылке, платформа обязательно будет сама возвращать на клиента, чтобы обеспечить поведение, описанное в начале статьи. Если параметр не нужно возвращать, то будет перерасход трафика. Для оптимизации обмена данными параметры, значения которых нам не нужны на выходе, нужно помечать словом Знач.

Здесь примечателен второй пункт. Для оптимизации трафика платформа не будет возвращать значение параметра на клиент, если параметр помечен словом Знач. Все это замечательно, но приводит к интересному эффекту.

Как я уже говорил, при передаче объекта на сервер происходит сериализация, т.е. выполняется "глубокая" копия объекта. А при наличии слова Знач объект не поедет с сервера обратно на клиента. Складываем эти два факта и получаем следующее:

&НаСервере Процедура ПоСсылке(Параметр) Параметр.Очистить(); КонецПроцедуры &НаСервере Процедура ПоЗначению(Знач Параметр) Параметр.Очистить(); КонецПроцедуры &НаКлиенте Процедура ПоЗначениюКлиент(Знач Параметр) Параметр.Очистить(); КонецПроцедуры &НаКлиенте Процедура ПроверитьЗнач() Список1= Новый СписокЗначений; Список1.Добавить("привет"); Список2 = Список1.Скопировать(); Список3 = Список1.Скопировать(); // объект копируется полностью, // передается на сервер, потом возвращается. // очистка списка видна в точке вызова ПоСсылке(Список1); // объект копируется полностью, // передается на сервер. Назад не возвращается. // Очистка списка НЕ ВИДНА в точке вызова ПоЗначению(Список2); // копируется только указатель объекта // очистка списка видна в точке вызова ПоЗначениюКлиент(Список3); Сообщить(Список1.Количество()); Сообщить(Список2.Количество()); Сообщить(Список3.Количество()); КонецПроцедуры

Резюме

Если вкратце, то резюмировать можно следующим образом:

  • Передача по ссылке позволяет "затереть" объект совсем другим объектом
  • Передача по значению не позволяет "затереть" объект, но изменения внутреннего состояния объекта будут видны, т.к. идет работа с одним и тем же экземпляром объекта
  • При серверном вызове работа идет с РАЗНЫМИ экземлярами объекта, т.к. выполнялось глубокое копирование. Ключевое слово Знач запретит копирование серверного экземпляра обратно в клиентский, и изменение внутреннего состояния объекта на сервере не приведет к аналогичному изменению на клиенте.

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

Итак, пусть Factorial(n) – это функция вычисления факториала числаn . Тогда, учитывая, что нам «известен» факториал 1 – это 1, можно построить следующую цепочку:

Factorial(4)=Factorial(3)*4

Factorial(3)=Factorial(2)*3

Factorial(2)=Factorial(1)*2

Но, если бы у нас не было терминального условия, что при n=1 функцияFactorial должна возвратить 1, то такая теоретически цепочка никогда бы не завершилась, и это могло ошибкойCall Stack Overflow – переполнение стека вызова. Чтобы понять что такое стек вызова и как он может переполниться, давайте посмотрим на рекурсивную реализацию нашей функции:

Function factorial(n: Integer): LongInt;

If n=1 Then

Factorial:=Factorial(n-1)*n;

End;

Как мы видим, для того, чтобы цепочка работала корректно, необходимо перед каждым очередным вызовом функции самой себя, где-то сохранять все локальные переменные, чтобы затем при обратном «разворачивании» цепочки результат был правильным (вычисленное значение факториала от n-1 умножилось наn ). В нашем случае – при каждом вызове функцииfactorial из самой себя, должны сохраняться все значения переменнойn . Область, в которой сохраняются локальные переменные функции при рекурсивном обращении к самой себе, называется стеком вызова. Разумеется, этот стек не бесконечный и при неправильном построении рекурсивных вызовов может быть исчерпан. Конечность итераций нашего примера гарантируется тем, что приn=1 вызов функции прекратится.

Передача параметров по значению и по ссылке

До сих пор мы не могли изменить в подпрограмме значение фактического параметра (т.е. того параметра, который указывается при вызове подпрограммы), а в некоторых прикладных задачах это было бы удобным. Вспомним процедуруVal , которая изменяет значение сразу двух ее фактических параметров: первый – это тот параметр, куда будет записано преобразованное значение строковой переменной, а второй – это параметрCode , куда помещается номер ошибочного символа, в случае неудачи при преобразовании типа. Т.е. все-таки существует механизм, при помощи которого подпрограмма может изменять фактические параметры. Это возможно благодаря различным способам передачи параметров. Давайте разберемся детально в этих способах.

Программирование на языке Pascal

Передача параметров по значению

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

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

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

Передача параметров по ссылке

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

Для того, чтобы указать, что какой-либо аргумент необходимо передать по ссылке, перед его объявлением добавляется ключевое словоvar :

Procedure getTwoRandom(var n1, n2:Integer; range: Integer);

n1:=random(range);

n2:=random(range); end ;

var rand1, rand2: Integer;

Begin getTwoRandom(rand1,rand2,10); WriteLn(rand1); WriteLn(rand2);

End.

В этом примере, в процедуру getTwoRandom в качестве фактических параметров передаются ссылки на две переменные:rand1 иrand2 . Третий фактический параметр (10) передается по значению. Процедура записывает посредством формальных параметров

Программы