假设我有一个班级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
非空,则会浪费。我可以以某种方式解决这个问题吗?
更新:我正在考虑可能在getOrElse
的参数类型(左值/右值)上重载,但还没弄清楚到底是怎么做的。
更新2:也许这个?
T getOrElse(T &&x) const { ... }
const T & getOrElse(const T &x) const { ... }
但我认为这可能是模棱两可的,因为左值和右值都适合第二个版本。
答案 0 :(得分:4)
但是,我认为以下代码不安全:
Option<int> x; x.getOrElse(123); // reference to temporary variable!
你是对的。这就是std::optional::value_or()
返回T
而不是T&
或T const&
的原因。根据{{3}}中的基本原理:
有人认为函数应该通过常量引用而不是值返回,这样可以避免在某些情况下的复制开销:
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!
我建议你遵循相同的指导方针。如果您确实需要避免复制,请使用三元组。这是安全无副本的:
Optional<int> ox = ...;
const int& val = ox ? *ox : 123;
如果你真的没有,或者Optional
无论如何都是左值,getOrElse()
更简洁。
答案 1 :(得分: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
,并且可以轻松避免存储重复项。
答案 2 :(得分:0)
我可以建议重新设计这门课吗?
它有一个默认的ctor,它可以让val_为nullptr,但是它同时有一个get()因为取消引用(*)而可能抛出异常。它还设计为在shared_prt中保存T,但将其作为参考返回。
让客户知道它为空:
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();
为什么我删除:if(val_ == nullptr){
让make_shared&lt;&gt;明确:
所以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_;
};
答案 3 :(得分:0)
我宁愿通过引用返回,让调用者决定是否要存储引用或返回值的副本。