>
>
>
V835. Passing cheap-to-copy argument by…


V835. Passing cheap-to-copy argument by reference may lead to decreased performance.

The analyzer detected that function argument is passed by reference to const. But it is better to pass the argument by copy.

Let's look at two examples for 64-bit systems.

In the first example, an object of the 'std::string_view' type is passed by reference to const:

uint32_t foo_reference(const std::string_view &name) noexcept
{
  return static_cast<uint32_t>(8 + name.size()) + name[0];
}

Assembly code:

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

Here, every time data is read from the object of type 'const std::string_view &', a dereference occurs. You can see this in instructions 'mov eax, dword ptr [rdi]' (1) and 'mov rcx, qword ptr [rdi + 8] ' (2).

In the second example, the object is passed by copy:

uint32_t foo_value(std::string_view name) noexcept
{
  return static_cast<uint32_t>(8 + name.size()) + name[0];
}

Assembly code:

foo_value(std::basic_string_view<char, std::char_traits<char> >):
        movsx   eax, byte ptr [rsi]
        add     eax, edi
        add     eax, 8
        ret

The compiler generated less code for the second example. This happens because the object is placed in the CPU registers and there is no need for addressing to access this object.

Let's investigate what objects to pass by copy and what object to pass by reference.

To do this, we need to read "System V Application Binary Interface AMD64 Architecture Processor Supplement". This document describes the calling conventions for the Unix-like OSs. Paragraph 3.2.3 describes how the parameters are passed. For each parameter, a separate class is defined. If a parameter has the MEMORY class, then it will be passed through the stack. Otherwise, the parameter is passed through the CPU registers, as in the example above. According to subparagraph 5 (C), if the object's size exceeds 16 bytes, then it has the MEMORY class. The exception is aggregate types up to 64 bytes in size, the first field of which is SSE, and all other fields are SSEUP. This means objects with a larger size will be placed on the function call stack. To access them, you need addressing.

Let's look at two more examples for 64-bit systems.

In the third one, a 16-byte object is passed by a copy.

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;
}

Assembly code:

foo_16(SixteenBytes):                    # @foo_16(SixteenBytes)
        lea     eax, [rsi + rdi]
        ret

The compiler generated efficient code by placing a structure in two 64-bit registers.

In the second example, a 24-byte structure is passed by the copy:

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;
}

Assembly code:

foo_more_than_16(MoreThanSixteenBytes):
        mov     eax, dword ptr [rsp + 16]
        add     eax, dword ptr [rsp + 8]
        add     eax, dword ptr [rsp + 24]
        ret

According to the calling convention, the compiler must place the structure on the stack. This leads to the fact that the structure is accessed indirectly, through the address, which is calculated with the 'rsp' register. In such a case, the V813 warning will be issued.

Windows has similar calling convention. You can read more in the documentation.

The diagnostic is disabled on a 32-bit x86 platform since the calling conventions are different — there are not enough CPU registers to pass arguments.

The diagnostic may issue false positives. References to const may have unusual ways of use. For example, a function to which such a reference is passed can save it to the global storage. And the object to which the reference refers may change.

Look at the example:

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();
}

The 'foo' function calls the 'SafeReference' function and passes it the 'value' variable as a parameter by reference to const. Then this reference is saved to the global 'rst' storage. In this case, the variable 'value' can change since it is not a const itself.

The code above is rather unnatural and poorly written. Real projects may have more complex cases. If you know what you're doing, you can suppress the diagnostics with a special comment: '//-V835'.

If your project has a lot of such places, you can completely disable diagnostics by adding '//-V::835' to the precompiled header or '.pvsconfig' file. You can read more about suppressing false positives in the documentation.