Владение C ++ параметром функции - PullRequest
4 голосов
/ 18 июня 2019

Я написал функцию, которая имеет такую ​​форму:

Result f( const IParameter& p);

Мое намерение состоит в том, чтобы эта подпись прояснила, что функция не вступает во владение параметром p.

Проблема в том, что Result будет сохранять ссылку на IParameter:

class Result
{  
    const IParameter& m_p;

public: 
    Result( const IParameter& p ) 
        : m_p( p ){ } 
};

Но потом случилось так, что кто-то вызвал функцию следующим образом:

const auto r = f(ConcreteParameter{});

К сожалению, временный объект может быть связан с константной ссылкой, и это вызвало сбой.

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

Ответы [ 5 ]

8 голосов
/ 18 июня 2019

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

Result f( const IParameter&& ) = delete;

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

class Result
{  
    const IParameter& m_p;

public: 
    Result( const IParameter& p ) 
        : m_p( p ){ } 
    Result( const IParameter&& ) = delete;
};
3 голосов
/ 18 июня 2019

Как правило, если функция получает значение на const&, ожидается, что функция будет использовать значение, но не будет его удерживать.Вы держите ссылку на значение, поэтому вам, вероятно, следует изменить тип аргумента на shared_ptr (если ресурс обязательный) или weak_ptr (если ресурс необязательный).В противном случае вы будете время от времени сталкиваться с такими проблемами, поскольку никто не читает документацию.

3 голосов
/ 18 июня 2019

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

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

doSomethingWithResult(Result{SomeParameterType{}});

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

Кроме того, удаление конструктора rvalue не предотвратит все случаи. Подумайте об этом:

auto make_result() -> Result {
    SomeParameterType param;
    return Result{param};
}

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

Так что, если вам все равно придется документировать такое поведение, я бы выбрал то, что стандартная библиотека делает с представлениями строк :

int main() {
    auto sv = std::string_view{std::string{"ub"}};
    std::cout << "This is " << sv;
}

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

3 голосов
/ 18 июня 2019

Вы можете вручную удалить конструктор, принимающий значение IParameter&& из набора перегрузки:

class Result
{
    // ...

    public:
    Result( IParameter&& ) = delete; // No temporaries!
    Result( const IParameter& p );
};

Когда клиентский код пытается создать экземпляр объекта через

Result f(ConcreteParameter{}); // Error

, конструктор принимаетconst -качественная ссылка не совпадает из-за отсутствия const -ness, но конструктор rvalue точно совпадает.Поскольку это = delete d, компилятор отказывается принимать создание такого объекта.

Обратите внимание, что, как указано в комментариях, это можно обойти с помощью const -качественных временных значений, см. @ Ответ NathanOliver о том, как этого избежать.

Также обратите внимание, что не все согласны с тем, что это хорошая практика, посмотрите здесь (в 15:20) например.

0 голосов
/ 19 июня 2019

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

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

void f(const A& a, const B& b)
{
    // function body here
}

void f(const A& a, B&& b) = delete;
void f(A&& a, const B& b) = delete;
void f(A&& a, B&& b) = delete;

Нам нужно delete все возможные комбинации, и это будет трудно поддерживать в долгосрочной перспективе.Поэтому мое предлагаемое решение состоит в том, чтобы воспользоваться тем фактом, что конструктор reference_wrapper, который переносит T при перемещении, уже удален в STD, а затем написать следующее:

using AConstRef = reference_wrapper<const A>;
using BConstRef = reference_wrapper<const B>;

void f(AConstRef a, BConstRef b)
{
    // function body here
}

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...