Подтвердить что ты не робот

Типы типов С++: pass by const & или по значению?

Это недавно появилось в обзоре обзора кода, но без удовлетворительного заключения. Типы, о которых идет речь, являются аналогами С++ string_view TS. Это простые обходные обертки вокруг указателя и длины, украшенные некоторыми пользовательскими функциями:

#include <cstddef>

class foo_view {
public:
    foo_view(const char* data, std::size_t len)
        : _data(data)
        , _len(len) {
    }

    // member functions related to viewing the 'foo' pointed to by '_data'.

private:
    const char* _data;
    std::size_t _len;
};

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

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

Аргументы в пользу pass-by-const-reference составляли "более идиоматический для передачи объектов const и amp;" и "вероятно, не менее эффективный".

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

Для этого вопроса можно с уверенностью предположить семантику С++ 11 или С++ 14 и достаточно современные инструментальные цепочки и целевые архитектуры и т.д.

4b9b3361

Ответ 1

Если вы сомневаетесь, перейдите по значению.

Теперь вы должны только редко сомневаться.

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

Причина, по которой вы должны переходить по значению, когда сомневаетесь, потому что значения легче рассуждать. Ссылка (даже a const one) на внешние данные может мутировать в середине алгоритма, когда вы вызываете обратный вызов функции или что у вас есть, делая то, что кажется простой функцией, в сложный беспорядок.

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

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

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

Ответ 2

EDIT: Код доступен здесь: https://github.com/acmorrow/stringview_param

Я создал некоторый пример кода, который, как представляется, демонстрирует, что значение pass-by-value для объектов типа string_view приводит к лучшему коду как для вызывающих, так и для определения функций на по меньшей мере одной платформе.

Сначала мы определяем поддельный класс string_view (у меня не было реальной вещи) в string_view.h:

#pragma once

#include <string>

class string_view {
public:
    string_view()
        : _data(nullptr)
        , _len(0) {
    }

    string_view(const char* data)
        : _data(data)
        , _len(strlen(data)) {
    }

    string_view(const std::string& data)
        : _data(data.data())
        , _len(data.length()) {
    }

    const char* data() const {
        return _data;
    }

    std::size_t len() const {
        return _len;
    }

private:
    const char* _data;
    size_t _len;
};

Теперь давайте определим некоторые функции, которые потребляют string_view, либо по значению, либо по ссылке. Вот подписи в example.hpp:

#pragma once

class string_view;

void __attribute__((visibility("default"))) use_as_value(string_view view);
void __attribute__((visibility("default"))) use_as_const_ref(const string_view& view);

Тела этих функций определяются следующим образом: example.cpp:

#include "example.hpp"

#include <cstdio>

#include "do_something_else.hpp"
#include "string_view.hpp"

void use_as_value(string_view view) {
    printf("%ld %ld %zu\n", strchr(view.data(), 'a') - view.data(), view.len(), strlen(view.data()));
    do_something_else();
    printf("%ld %ld %zu\n", strchr(view.data(), 'a') - view.data(), view.len(), strlen(view.data()));
}

void use_as_const_ref(const string_view& view) {
    printf("%ld %ld %zu\n", strchr(view.data(), 'a') - view.data(), view.len(), strlen(view.data()));
    do_something_else();
    printf("%ld %ld %zu\n", strchr(view.data(), 'a') - view.data(), view.len(), strlen(view.data()));
}

Функция do_something_else здесь - это stand-in для произвольных вызовов функций, которые компилятор не знает (например, функции от других динамических объектов и т.д.). Объявление находится в do_something_else.hpp:

#pragma once

void __attribute__((visibility("default"))) do_something_else();

И тривиальное определение находится в do_something_else.cpp:

#include "do_something_else.hpp"

#include <cstdio>

void do_something_else() {
    std::printf("Doing something\n");
}

Теперь мы компилируем do_something_else.cpp и example.cpp в отдельные динамические библиотеки. Компилятор здесь XCode 6 clang на OS X Yosemite 10.10.1:

clang++ -mmacosx-version-min=10.10 --stdlib=libc++ -O3 -flto -march=native -fvisibility-inlines-hidden -fvisibility=hidden --std=c++11 ./do_something_else.cpp -fPIC -shared -o libdo_something_else.dylib clang++ -mmacosx-version-min=10.10 --stdlib=libc++ -O3 -flto -march=native -fvisibility-inlines-hidden -fvisibility=hidden --std=c++11 ./example.cpp -fPIC -shared -o libexample.dylib -L. -ldo_something_else

Теперь мы разбираем libexample.dylib:

> otool -tVq ./libexample.dylib
./libexample.dylib:
(__TEXT,__text) section
__Z12use_as_value11string_view:
0000000000000d80    pushq   %rbp
0000000000000d81    movq    %rsp, %rbp
0000000000000d84    pushq   %r15
0000000000000d86    pushq   %r14
0000000000000d88    pushq   %r12
0000000000000d8a    pushq   %rbx
0000000000000d8b    movq    %rsi, %r14
0000000000000d8e    movq    %rdi, %rbx
0000000000000d91    movl    $0x61, %esi
0000000000000d96    callq   0xf42                   ## symbol stub for: _strchr
0000000000000d9b    movq    %rax, %r15
0000000000000d9e    subq    %rbx, %r15
0000000000000da1    movq    %rbx, %rdi
0000000000000da4    callq   0xf48                   ## symbol stub for: _strlen
0000000000000da9    movq    %rax, %rcx
0000000000000dac    leaq    0x1d5(%rip), %r12       ## literal pool for: "%ld %ld %zu\n"
0000000000000db3    xorl    %eax, %eax
0000000000000db5    movq    %r12, %rdi
0000000000000db8    movq    %r15, %rsi
0000000000000dbb    movq    %r14, %rdx
0000000000000dbe    callq   0xf3c                   ## symbol stub for: _printf
0000000000000dc3    callq   0xf36                   ## symbol stub for: __Z17do_something_elsev
0000000000000dc8    movl    $0x61, %esi
0000000000000dcd    movq    %rbx, %rdi
0000000000000dd0    callq   0xf42                   ## symbol stub for: _strchr
0000000000000dd5    movq    %rax, %r15
0000000000000dd8    subq    %rbx, %r15
0000000000000ddb    movq    %rbx, %rdi
0000000000000dde    callq   0xf48                   ## symbol stub for: _strlen
0000000000000de3    movq    %rax, %rcx
0000000000000de6    xorl    %eax, %eax
0000000000000de8    movq    %r12, %rdi
0000000000000deb    movq    %r15, %rsi
0000000000000dee    movq    %r14, %rdx
0000000000000df1    popq    %rbx
0000000000000df2    popq    %r12
0000000000000df4    popq    %r14
0000000000000df6    popq    %r15
0000000000000df8    popq    %rbp
0000000000000df9    jmp 0xf3c                   ## symbol stub for: _printf
0000000000000dfe    nop
__Z16use_as_const_refRK11string_view:
0000000000000e00    pushq   %rbp
0000000000000e01    movq    %rsp, %rbp
0000000000000e04    pushq   %r15
0000000000000e06    pushq   %r14
0000000000000e08    pushq   %r13
0000000000000e0a    pushq   %r12
0000000000000e0c    pushq   %rbx
0000000000000e0d    pushq   %rax
0000000000000e0e    movq    %rdi, %r14
0000000000000e11    movq    (%r14), %rbx
0000000000000e14    movl    $0x61, %esi
0000000000000e19    movq    %rbx, %rdi
0000000000000e1c    callq   0xf42                   ## symbol stub for: _strchr
0000000000000e21    movq    %rax, %r15
0000000000000e24    subq    %rbx, %r15
0000000000000e27    movq    0x8(%r14), %r12
0000000000000e2b    movq    %rbx, %rdi
0000000000000e2e    callq   0xf48                   ## symbol stub for: _strlen
0000000000000e33    movq    %rax, %rcx
0000000000000e36    leaq    0x14b(%rip), %r13       ## literal pool for: "%ld %ld %zu\n"
0000000000000e3d    xorl    %eax, %eax
0000000000000e3f    movq    %r13, %rdi
0000000000000e42    movq    %r15, %rsi
0000000000000e45    movq    %r12, %rdx
0000000000000e48    callq   0xf3c                   ## symbol stub for: _printf
0000000000000e4d    callq   0xf36                   ## symbol stub for: __Z17do_something_elsev
0000000000000e52    movq    (%r14), %rbx
0000000000000e55    movl    $0x61, %esi
0000000000000e5a    movq    %rbx, %rdi
0000000000000e5d    callq   0xf42                   ## symbol stub for: _strchr
0000000000000e62    movq    %rax, %r15
0000000000000e65    subq    %rbx, %r15
0000000000000e68    movq    0x8(%r14), %r14
0000000000000e6c    movq    %rbx, %rdi
0000000000000e6f    callq   0xf48                   ## symbol stub for: _strlen
0000000000000e74    movq    %rax, %rcx
0000000000000e77    xorl    %eax, %eax
0000000000000e79    movq    %r13, %rdi
0000000000000e7c    movq    %r15, %rsi
0000000000000e7f    movq    %r14, %rdx
0000000000000e82    addq    $0x8, %rsp
0000000000000e86    popq    %rbx
0000000000000e87    popq    %r12
0000000000000e89    popq    %r13
0000000000000e8b    popq    %r14
0000000000000e8d    popq    %r15
0000000000000e8f    popq    %rbp
0000000000000e90    jmp 0xf3c                   ## symbol stub for: _printf
0000000000000e95    nopw    %cs:(%rax,%rax)

Интересно, что версия по значению имеет несколько инструкций короче. Но это только функции тела. Как насчет абонентов?

Мы определим некоторые функции, которые вызывают эти две перегрузки, пересылая const std::string&, в example_users.hpp:

#pragma once

#include <string>

void __attribute__((visibility("default"))) forward_to_use_as_value(const std::string& str);
void __attribute__((visibility("default"))) forward_to_use_as_const_ref(const std::string& str);

И определите их в example_users.cpp:

#include "example_users.hpp"

#include "example.hpp"
#include "string_view.hpp"

void forward_to_use_as_value(const std::string& str) {
    use_as_value(str);
}

void forward_to_use_as_const_ref(const std::string& str) {
    use_as_const_ref(str);
}

Снова, мы скомпилируем example_users.cpp в общую библиотеку:

clang++ -mmacosx-version-min=10.10 --stdlib=libc++ -O3 -flto -march=native -fvisibility-inlines-hidden -fvisibility=hidden --std=c++11 ./example_users.cpp -fPIC -shared -o libexample_users.dylib -L. -lexample

И снова мы посмотрим на сгенерированный код:

> otool -tVq ./libexample_users.dylib
./libexample_users.dylib:
(__TEXT,__text) section
__Z23forward_to_use_as_valueRKNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE:
0000000000000e70    pushq   %rbp
0000000000000e71    movq    %rsp, %rbp
0000000000000e74    movzbl  (%rdi), %esi
0000000000000e77    testb   $0x1, %sil
0000000000000e7b    je  0xe8b
0000000000000e7d    movq    0x8(%rdi), %rsi
0000000000000e81    movq    0x10(%rdi), %rdi
0000000000000e85    popq    %rbp
0000000000000e86    jmp 0xf60                   ## symbol stub for: __Z12use_as_value11string_view
0000000000000e8b    incq    %rdi
0000000000000e8e    shrq    %rsi
0000000000000e91    popq    %rbp
0000000000000e92    jmp 0xf60                   ## symbol stub for: __Z12use_as_value11string_view
0000000000000e97    nopw    (%rax,%rax)
__Z27forward_to_use_as_const_refRKNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE:
0000000000000ea0    pushq   %rbp
0000000000000ea1    movq    %rsp, %rbp
0000000000000ea4    subq    $0x10, %rsp
0000000000000ea8    movzbl  (%rdi), %eax
0000000000000eab    testb   $0x1, %al
0000000000000ead    je  0xebd
0000000000000eaf    movq    0x10(%rdi), %rax
0000000000000eb3    movq    %rax, -0x10(%rbp)
0000000000000eb7    movq    0x8(%rdi), %rax
0000000000000ebb    jmp 0xec7
0000000000000ebd    incq    %rdi
0000000000000ec0    movq    %rdi, -0x10(%rbp)
0000000000000ec4    shrq    %rax
0000000000000ec7    movq    %rax, -0x8(%rbp)
0000000000000ecb    leaq    -0x10(%rbp), %rdi
0000000000000ecf    callq   0xf66                   ## symbol stub for: __Z16use_as_const_refRK11string_view
0000000000000ed4    addq    $0x10, %rsp
0000000000000ed8    popq    %rbp
0000000000000ed9    retq
0000000000000eda    nopw    (%rax,%rax)

И опять же, версия с поправкой на несколько инструкций короче.

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

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

Я отправлю код примера в github с помощью какой-то сборки script, чтобы другие могли протестировать их системы.

Но, основываясь на приведенном выше обсуждении и результатах проверки сгенерированного кода, мой вывод состоит в том, что метод pass-by-value - это способ поиска типов типов.

Ответ 3

Отложив философские вопросы о значении сигнализации const & -ness и стоимости в качестве параметров функции, мы можем взглянуть на некоторые последствия ABI для различных архитектур.

http://www.macieira.org/blog/2012/02/the-value-of-passing-by-value/ излагает некоторые решения и тестирование, выполненные некоторыми людьми QT на x86-64, ARMv7 hard-float, MIPS hard-float (o32) и IA-64. В основном, он проверяет, могут ли функции передавать различные структуры через регистры. Неудивительно, что каждая платформа может управлять двумя указателями по регистру. И учитывая, что sizeof (size_t), как правило, sizeof (void *), мало оснований полагать, что мы будем разливаться в памяти здесь.

Мы можем найти больше дерева для огня, учитывая такие предложения, как: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3538.html. Обратите внимание, что const ref имеет некоторые недостатки, а именно риск сглаживания, который может предотвратить важные оптимизации и требует дополнительных размышлений для программиста. В отсутствие поддержки С++ для ограничения C99 передача по значению может повысить производительность и снизить когнитивную нагрузку.

Предположим, что я синтезирую два аргумента в пользу pass by value:

  • Для 32-разрядных платформ часто не хватало возможности передавать две текстовые структуры по регистру. Это больше не является проблемой.
  • ссылки const количественно и качественно хуже значений, поскольку они могут быть псевдонимом.

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

Ответ 4

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

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

  • Оптимизатор предполагает, что аргументы, переданные с помощью ссылки или ссылки на константу, могут быть изменены (const не имеет значения из-за const_cast) или упоминается глобальным указателем или изменен другим потоком. В принципе, аргументы, переданные по ссылке, становятся "отравленными" значениями на сайте вызова, которые оптимизатор не может применить к другим оптимизациям.
  • В запросе, если есть несколько аргументов reference/pointer одного и того же базового типа, оптимизатор предполагает, что они псевдонимы с чем-то другим, и это снова исключает многие оптимизации.

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

Для детального рассмотрения предмета я не могу рекомендовать достаточно Chandler Carruth: оптимизация появляющихся структур С++. Первой точкой обсуждения является то, что "людям нужно изменить голову о передаче по значению... модель регистров прохождения аргументов устарела".

Ответ 5

Вот мои правила для передачи переменных в функции:

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

Надеюсь, что это поможет.

Ответ 6

Значение представляет собой значение, а ссылка const является ссылкой на константу.

Если объект не является неизменным, то два являются эквивалентными понятиями НЕ.

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

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

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

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

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

Указание параметра const T&, когда вызываемый пользователь не заинтересован в идентичности (т.е. изменения состояния будущего *), является ошибкой проектирования. Единственное оправдание для этой ошибки преднамеренно - это когда объект тяжелый, а создание копии - серьезная проблема с производительностью.

Для небольших объектов изготовление копий часто на самом деле лучше с точки зрения производительности, потому что есть одна косвенность меньше, а стороне параноида оптимизатора не нужно рассматривать проблемы сглаживания. Например, если у вас есть F(const X& a, Y& b) и X содержит член типа Y, оптимизатор будет вынужден рассмотреть возможность того, что ссылка не-const фактически привязана к этому под-объекту X.

(*) С "будущим" я включаю в себя как после возвращения из метода (т.е. вызывающий хранит адрес объекта и запоминает его), так и во время выполнения кода вызываемого абонента (т.е. сглаживания).

Ответ 7

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

Ответ 8

Моим аргументом было бы использовать оба. Предпочитают const &. Это также становится документацией. Если вы объявили его как const и amp;, тогда компилятор будет жаловаться, если вы попытаетесь изменить экземпляр (когда вы этого не сделали). Если вы намерены изменить его, тогда возьмите его по значению. Но таким образом вы явно сообщаете будущим разработчикам, что вы намерены изменить экземпляр. И const & "вероятно, не хуже", чем по стоимости, и потенциально намного лучше (если построение экземпляра дорогое, и у вас его еще нет).