2016-05-03 3 views
8

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

template<typename T> 
class Option { 
public: 
    Option() noexcept 
    {} 

    Option(T val) noexcept : val_(std::make_shared<T>(std::move(val))) 
    {} 

    const T & get() const 
    { 
     if (val_ == nullptr) { 
      throw std::out_of_range("get on empty Option"); 
     } 
     return *val_; 
    } 

    const T & getOrElse(const T &x) const 
    { 
     return val_ == nullptr ? x : *val_; 
    } 

private: 
    std::shared_ptr<T> val_; 
}; 

Аргумент, передаваемый Option::getOrElse это значение по умолчанию, чтобы вернуться, когда этот Option пуст:

Option<int> x; // empty 
int y = 123; 
x.getOrElse(y); // == 123 

Однако я что следующий код не является безопасным:

Option<int> x; 
x.getOrElse(123); // reference to temporary variable! 

Более безопасным способом было бы вернуть значение от Option::getOrElse, но это было бы расточительно, если Option не пуст. Могу ли я как-то обойти это?

UPDATE: Я думаю о том, возможно, перегрузках по типу аргумента (именующий/RValue) из getOrElse, но еще не выяснил, как именно это сделать.

ОБНОВЛЕНИЕ 2: Возможно это?

T getOrElse(T &&x) const { ... } 

const T & getOrElse(const T &x) const { ... } 

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

+0

Почему вы ничего не возвращаете? Если это проверка успеха или неудачи, вы можете вернуть «bool». – NathanOliver

+1

@NathanOliver Я не уверен, что вы имеете в виду ...? 'getOrElse' - это getter со значением по умолчанию. –

+0

Я не вижу значения по умолчанию в вашем коде. Вы пытаетесь объединить геттер и сеттер в одну функцию? – NathanOliver

ответ

4

Однако, я думаю, что следующий код не является безопасным:

Option<int> x; 
x.getOrElse(123); // reference to temporary variable! 

Вы правильно. Вот почему std::optional::value_or() возвращает T, а не T& или T const&. В соответствии с обоснованием в N3672:

Утверждалось, что функция должна возвращать постоянной ссылке, а не стоимости, что позволит избежать копирования накладных расходов в определенных ситуациях:

void observe(const X& x); 

optional<X> ox { /* ... */ }; 
observe(ox.value_or(X{args})); // unnecessary copy 

Однако преимущество функция value_or видна только тогда, когда дополнительный объект предоставляется как временный (без имени); в противном случае, тройной оператор одинаково полезен:

optional<X> ox { /* ... */ }; 
observe(ox ? *ok : X{args});   // no copy 

Кроме того, возвращаясь в качестве ссылки будет, вероятно, вынести оборванную ссылку, в случае, если дополнительный объект отключен, так как второй аргумент, как правило, временный:

optional<X> ox {nullopt}; 
auto&& x = ox.value_or(X{args}); 
cout << x;        // x is dangling! 

Предлагаю вам следовать тем же самым указаниям. Если вам действительно нужно избегать копирования, используйте тройной. Это безопасно и copyless:

Optional<int> ox = ...; 
const int& val = ox ? *ox : 123; 

Если вы действительно не делаете, или Optional является Rvalue в любом случае, getOrElse() более кратким.

+0

Я не понимаю, почему вы сказали его пример небезопасен - или то, что рационально для того, чтобы не возвращать const. Я читал то, что вы дважды вставляли, но все равно не понимаете. – xaxxon

+1

> возврат по ссылке, скорее всего, приведет к обманутой ссылке, в случае, если необязательный объект отключен - я не знаю, что означает «отключенный». – xaxxon

+0

@xaxxon «Отключено» Я думаю, что Барри означает «пусто». –

0

Поскольку пользователи класса могут ожидать ссылки вернулись из Option::get() быть действительными только вместе, как конкретный экземпляр срока службы Option объекта, вы могли бы разумно сделать то же самое ожидание, что возвращается из Option::getOrElse().

В этом случае это может быть приемлемым накладные расходы на объект, чтобы сохранить коллекцию вещей, которые он должен поддерживать для клиента:

#include <list> 
#include <memory> 
#include <iostream> 

template<typename T> 
class Option { 
public: 
    Option() noexcept 
    {} 

    Option(T val) noexcept : val_(std::make_shared<T>(std::move(val))) 
    {} 

    const T & get() const 
    { 
     if (val_ == nullptr) { 
      throw std::out_of_range("get on empty Option"); 
     } 
     return *val_; 
    } 

    const T & getOrElse(const T &x) const 
    { 
     if (val_ == nullptr) { 
      std::cout << "storing const T &\n"; 
      elses_.push_front(x); 
      return elses_.front(); 
     } 

     return *val_; 
    } 

    const T & getOrElse(T &&x) const 
    { 
     if (val_ == nullptr) { 
      std::cout << "storing T && by move\n"; 
      elses_.push_front(std::move(x)); 
      return elses_.front(); 
     } 

     return *val_; 
    } 


private: 
    std::shared_ptr<T> val_; 
    mutable std::list<T> elses_; 
}; 


int main() 
{ 
    Option<int> x; // empty 
    int y = 123; 
    auto rx = x.getOrElse(y); // == 123 

    auto & rxx = x.getOrElse(42); 

    std::cout << "rx = " << rx << "\n"; 
    std::cout << "rxx = " << rxx << "\n"; 
} 

Ссылки, возвращаемые Option::getOrElse() будет действителен до тех пор, так как ссылка вернулась с Option::get(). Конечно, это также означает, что Option::getOrElse() может генерировать исключение.

В небольшое улучшение, если T типа можно использовать в качестве ключей для ассоциативного контейнера можно использовать один из тех, кто вместо std::list и легко избежать хранения дубликатов.

+0

Интересный подход! –

+0

Нам действительно нужно хранить 'const T &' s в 'elses_' также? –

+0

Для решения несуществующей проблемы в одной функции-члена вы предлагаете хранилище типов * целый дополнительный контейнер * и теперь 'getOrElse()' потенциально выделяет память ?! – Barry

0

Могу ли я предложить перепроектировать этот класс?

У этого есть значение по умолчанию ctor, которое может оставить val_ равным nullptr, но оно имеет get() в то же время, которое может вызывать исключение из-за разыменования (*). Он также предназначен для сохранения T в shared_prt, но возвращает его как ссылку.

Пусть клиент знать, что это нуль:

template<typename T> 
class Option { 
public: 
    Option() noexcept 
    {} 

    Option(T val) noexcept : val_(std::make_shared<T>(std::move(val))) 
    {} 

    const T & get() const 
    { 
     return *val_; 
    } 

    bool IsNull() const 
    { 
     return val_ == nullptr; 
    } 

private: 
    std::shared_ptr<T> val_; 
}; 

Код клиента изменен с

Option option; 
const T & ref = option.getOrElse(123); 

быть:

Option option; 
const T & ref = option.IsNull() ? 123 : option.get(); 

Почему я удалить: если (val_ == nullptr) {

Давайте сделаем make_shared <> ясно:

  1. возвращения действительного указателя или
  2. броска bad_alloc исключения; не возвращает нуль

Так IsNull() тоже бесполезно, оно должно быть как:

template<typename T> 
class Option { 
public: 
    Option(T val) noexcept : val_(std::make_shared<T>(std::move(val))) 
    {} 

    const T & get() const 
    { 
     return *val_; 
    } 

private: 
    std::shared_ptr<T> val_; 
}; 

Зачем использовать shared_ptr? объекты опции могут перемещаться или копироваться несколько раз? или же я предпочитаю проектировать это нравится:

template<typename T> 
class Option { 
public: 
    Option(T val) noexcept : val_(std::move(val)) 
    {} 

    const T & get() const 
    { 
     return val_; 
    } 

private: 
    T val_; 
}; 
0

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

Смежные вопросы