Анализатор обнаружил функцию, которая принимает параметр по ссылке на константный объект, когда эффективнее это делать по копии.
Рассмотрим два примера для 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' файл. Подробнее о подавлении ложных предупреждений можно прочитать в документации.