Анализатор обнаружил функцию, которая принимает параметр по ссылке на константный объект, когда эффективнее это делать по копии.
Рассмотрим два примера для 64-битных систем.
В первом — функция принимает объекты типа 'const std::string_view &':
uint32_t foo_reference(const std::string_view &name) noexcept { return static_cast<uint32_t>(8 + name.size()) + name[0]; }
Ассемблерный код:
foo_reference(std::basic_string_view<char, std::char_traits<char> > const&): mov eax, dword ptr [rdi] // <= (1) mov rcx, qword ptr [rdi + 8] // <= (2) movsx ecx, byte ptr [rcx] add eax, ecx add eax, 8 ret
В нем при каждом чтении данных из объекта типа 'const std::string_view &' происходит разыменование. Это инструкции 'mov eax, dword ptr [rdi]' (1) и 'mov rcx, qword ptr [rdi + 8] ' (2).
Во втором — функция принимает объекты типа 'std::string_view':
uint32_t foo_value(std::string_view name) noexcept { return static_cast<uint32_t>(8 + name.size()) + name[0]; }
Ассемблерный код:
foo_value(std::basic_string_view<char, std::char_traits<char> >): movsx eax, byte ptr [rsi] add eax, edi add eax, 8 ret
Компилятор сгенерировал меньше кода для второго примера. Так происходит потому, что объект полностью помещается в регистры процессора и нет необходимости в адресации для доступа к нему.
Давайте разберёмся, какие объекты выгоднее передавать по копии, а какие по ссылке.
Обратимся к документу "System V Application Binary Interface AMD64 Architecture Processor Supplement". В нём описаны соглашения о вызовах функций для Unix-подобных систем. Пункт 3.2.3 описывает передачу параметров. Для каждого из них определяется свой класс. Если параметр имеет класс MEMORY, то он будет передаваться через стек. В противном случае параметр передаётся через регистры процессора, как в приведённом выше примере. Согласно подпункту 5 (C), если размер объекта превышает 16 байт, то он имеет класс MEMORY. Исключение составляют агрегатные типы размером до 64 байтов, первое поле которых имеют класс SSE, а все остальные SSEUP. Это означает, что объекты, имеющие больший размер, будут размещаться на стеке вызова функции, и для доступа к ним также необходима адресация.
Давайте рассмотрим ещё два примера для 64-битных систем.
В третьем — по копии принимается объект размером в 16 байт:
struct SixteenBytes { int64_t firstHalf; // 8-byte int64_t secondHalf; // 8-byte }; // 16-bytes uint32_t foo_16(SixteenBytes obj) noexcept { return obj.firstHalf + obj.secondHalf; }
Ассемблерный код:
foo_16(SixteenBytes): # @foo_16(SixteenBytes) lea eax, [rsi + rdi] ret
Компилятор сгенерировал эффективный код, разместив структуру в двух 64-битных регистрах.
Во четвертом примере по копии принимается структура размером в 24 байта:
struct MoreThanSixteenBytes { int64_t firstHalf; // 8-byte int64_t secondHalf; // 8-byte int32_t yetAnotherStuff; // 4-byte }; // 24-bytes uint32_t foo_more_than_16(MoreThanSixteenBytes obj) noexcept { return obj.firstHalf + obj.secondHalf + obj.yetAnotherStuff; }
Ассемблерный код:
foo_more_than_16(MoreThanSixteenBytes): mov eax, dword ptr [rsp + 16] add eax, dword ptr [rsp + 8] add eax, dword ptr [rsp + 24] ret
Согласно соглашению о вызовах, компилятор вынужден разместить структуру на стеке. Это приводит к тому, что доступ к ней происходит косвенно, через адрес, который вычисляется с помощью регистра 'rsp'. В таком случае будет выдано предупреждение V813.
На Windows аналогичные правила вызовов функций. Подробнее можно почитать в документации.
Диагностика отключена на 32-битной платформе x86, так как на ней правила вызова функций отличаются в силу того, что не хватает регистров процессора для передачи аргументов.
У диагностики возможны ложные срабатывания. У ссылок на константные объекты могут быть необычные применения. Например, функция, в которую передаётся некий объект по ссылке, может сохранить её в глобальное хранилище. При этом сам объект, на который она ссылается, может изменяться.
Рассмотрим пример:
struct RefStorage { const int &m_value; RefStorage(const int &value) : m_value { value } {} RefStorage(const RefStorage &value) : m_value { value.m_value } {} }; std::shared_ptr<RefStorage> rst; void SafeReference(const int &ref) { rst = std::make_shared<RefStorage>(ref); } void PrintReference() { if (rst) { std::cout << rst->m_value << std::endl; } } void foo() { int value = 10; SafeReference(value); PrintReference(); ++value; PrintReference(); }
Функция 'foo' вызывает функцию 'SafeReference' и передаёт ей в качестве параметра переменную 'value' по ссылке. Далее эта ссылка сохраняется в глобальное хранилище 'rst'. При этом переменная 'value' может изменяться, так как она сама не константная.
Приведённый код достаточно неестественный и плохо написан. В реальных проектах могут быть и более сложные случаи. Если программист знает, что делает, то диагностику можно подавить специальным комментарием '//-V835'.
Если в вашем проекте много таких мест, можно полностью отключить диагностику, добавив комментарий '//-V::835' в предкомпилированный заголовок или '.pvsconfig' файл. Подробнее о подавлении ложных предупреждений можно прочитать в документации.