Понедельник, 08 февраля 2021 19:00

Передача аргументов в функцию. Работа над ошибками с помощью статического анализатора кода. Часть 2.

Россия
Оцените материал
(0 голосов)

Сегодня мы рассмотрим причину появления сообщения:

Function parameter 'xxxxxxxx' should be passed by const reference.

от статического анализатора Cppcheck. 

Так же будут рассмотрены:

  • Передача аргументов в функцию по значению и по ссылке.
  • Использование const при передаче аргумента в функцию. 
  • Влияние разных способов передачи аргументов в функцию на время выполнения функции.

Function parameter 'xxxxxxxx' should be passed by const reference.

Данное сообщение означает, что вместо

int funcname(int value)

мы должны использовать

int funcname(const int &value)

Почему? Всё очень просто, каждый раз, когда вы передаете аргумент в функцию по значению, вот так:

int funcname(int value)

В памяти создаётся копия передаваемой переменной. Таким образом память используется неэффективно и впустую тратятся ресурсы.

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

Давайте проверим, как это работает, напишем простой проект:

void f(int a, int b)
{
    a = a * 2;
    b = b * 2;
    qDebug() << "In f(): a = " << a << ", b = " << b;
}

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);

    int a = 2;
    int b = 3;


    qDebug() << "Before f(): a = " << a << ", b = " << b;

    f(a,b);

    qDebug() << "After f(): a = " << a << ", b = " << b;

    app.quit(); 
}

Запустим: 

Before f(): a =  2 , b =  3
In f(): a =  4 , b =  6
After f(): a =  2 , b =  3

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

Давайте воспользуемся передачей по ссылке:

void f(int &a, int &b)
{
    a = a * 2;
    b = b * 2;
    qDebug() << "In f(): a = " << a << ", b = " << b;
}

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);

    int a = 2;
    int b = 3;

    qDebug() << "Before f(): a = " << a << ", b = " << b;

    f(a,b);

    qDebug() << "After f(): a = " << a << ", b = " << b;

    app.quit(); 
}

Запустим:

Before f(): a =  2 , b =  3
In f(): a =  4 , b =  6
After f(): a =  4 , b =  6

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

void fs(QString &str)
{
    str.append(";");
}

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);

    QString str = "123";

    qDebug() << "Before: " << str;

    fs(str);

    qDebug() << "After: " << str;

    app.quit(); 
}

Запустим:

Before:  "123"
After:  "123;"

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

Использование const

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

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

void f(const int &a, const int &b)
{
    a = a * 2;
    b = b * 2;
    qDebug() << "In f(): a = " << a << ", b = " << b;
}

Тогда на строках:

    a = a * 2;
    b = b * 2;

Вы получите сообщение об ошибке:

cannot assign to variable 'a' with const-qualified type 'const int &'
cannot assign to variable 'b' with const-qualified type 'const int &'

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

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

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

Возвращаем несколько значений при вызове функций

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

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

void fs(QString &str1, QString &str2, QString &str3)
{
    str1.append(";");
    str2.append("|");
    str3.append("_");
}

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);

    QString str1 = "123";
    QString str2 = "456";
    QString str3 = "789";

    qDebug() << "Before: " << str1 << " }{ " << str2 << " }{ " << str3;

    fs(str1, str2, str3);

    qDebug() << "After: " << str1 << " }{ " << str2 << " }{ " << str3;



    app.quit();
}

Запустим:

Before:  "123"  }{  "456"  }{  "789"
After:  "123;"  }{  "456|"  }{  "789_"

Таким образом мы "вернули" из функции значения трех переменных.

Потеря производительности при использовании передачи аргументов по значению

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

Создадим консольный проект Qt5, чтобы максимально снизить влияние Фреймворка на производительность.

Для теста напишем простые функции:

int f1(int value)
{
    if (value>0)
    {
        return 1;
    } else {
        return 0;
    }
}

int f2(const int &value)
{
    if (value>0)
    {
        return 1;
    } else {
        return 0;
    }
}

int f3(int *value)
{
    if (*value>0)
    {
        return 1;
    } else {
        return 0;
    }
}

Все функции проверяют значение и, если оно больше нуля возвращают 1, иначе возвращают 0.

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

Добавим код для проверки в функцию main:

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);

    QElapsedTimer timer;


    int t1=0, t2=0, t3=0;
    timer.start();
    for (int i=0; i < 50000000; i++)
    {
        f1(i);
    }
    t1 = timer.elapsed();

    timer.restart();
    for (int i=0; i < 50000000; i++)
    {
        f2(i);
    }
    t2 = timer.elapsed();

    timer.restart();
    for (int i=0; i < 50000000; i++)
    {
        f3(&i);
    }
    t3 = timer.elapsed();

    std::cout << t1 << ";" << t2 << ";" << t3 << "\n";

    app.quit();
}

Запустим:

100;102;110

Давайте создадим .cmd файл для автоматического сбора статистики:

@echo off
del result.csv
echo Running tests...
FOR /L %%p IN (1,1,100) DO echo %%p & ConstVarTest.exe >> result.csv

Запустим. Откроем результаты в Excel и построим диаграмму по среднему результату:

2021-02-04_14-39-03.png

Результаты у нас получились довольно неожиданные – разница между тремя методами всего несколько процентов.

В сети я нашел вот эту статью - https://theartofmachinery.com/2019/08/12/c_const_isnt_for_performance.html

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

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

Так что использование

int funcname(const int &value)

Является хорошей практикой, так как защищает от случайных ошибок.

Заключение

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

При передаче по значению создается копия переменной.

При передаче по ссылке – передается ссылка на область данных.

Рассмотрели способ передачи нескольких значений из функции.

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

Так же использование const не влияет на производительность программы, но помогает избежать неявных ошибок.

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

Прочитано 496 раз Последнее изменение Понедельник, 15 февраля 2021 12:25