这里需要显式调用析构函数吗?

3

我有一个类似于optional的类型(不能使用optional因为它是C++17的特性):

template <typename T>
struct maybe {
    maybe()        : valid_(false) {}
    maybe(T value) : valid_(true)  { new (&value_) T(value); }

    // destructor
    ~maybe() {
        if (valid_) {
            value_.~T();
        }
    }

    // move value out of maybe
    operator T&&() && {
        valid_ = false;
        return std::move(value());
    }

    // explicit validity 
    explicit operator bool() const {
        return valid_;
    }

    T& value() {
        if (!valid_) {
            throw std::runtime_error("boom");
        }
        return value_;
    }

private:
    union {
        T value_;
    };
    bool valid_;
};

我对 operator T&& 感到好奇,如果只是移动值是否不合适,因为析构函数将不再执行。看起来我需要将值移动到临时变量中,销毁我的存储,然后返回。哪种方式是正确的?


2
“valid_”应该保持为“true”。通过返回对值的右值引用,意味着该值可能很快被移动,但仍是“T”的一个实例。由于没有简单的方法来检测何时实际上被移动,因此简单的解决方案是不要触摸“valid_”,并让移动后的实例在析构函数中正常被销毁。 - François Andrieux
2个回答

3
不需要手动调用析构函数。移动(Move)仅用于调用接受右值引用的重载函数。如果调用代码对函数结果什么也不做,则此操作将是空操作。即使调用代码实际上对值进行了更改,它仍应保持在某种有效(但未指定)状态,并且仍然需要正确地析构(当maybe对象被销毁时这将发生)。

有道理,谢谢。我将在操作符被调用时省略使其无效,并仅记录在从中std::move()后访问maybe是未定义的。 - gct
@SeanMcAllister 为了调用你的运算符,你需要在一个maybe的右值引用上进行操作。之后,用户应该已经处于一个不能真正期望他们的maybe再持有一个值的情况下了。 - François Andrieux
@FrançoisAndrieux 这就是为什么 valid_ 被设置为 false 的原因,不是吗? - user7860670
@VTT valid_ 似乎表示 value_ 是否包含一个对象。即使是移动后的对象仍然是一个对象。就 valid_ 的使用而言,我认为它仍然是有效的。 - François Andrieux

1
// move value out of maybe
operator T&&() && {
    valid_ = false;
    return std::move(value());
}

你的问题在于 valid_ = false; 这一行是错误的。
你的类有一个不变量,即它要么包含一个值且 valid_true,要么不包含值且 valid_false
你的 operator T&&()&& 违反了这个不变量。
可选方案包括:
// move value out of maybe
operator T&&() && {
    return std::move(value());
}

或者

// move value out of maybe
operator T() && {
  T tmp = std::move(value());
  value().~T();
  valid_ = false;
  return tmp;
}

std::optional使用第一个,第二个也是可能的。

顺带一提:

maybe(T value) : valid_(true)  { new (&value_) T(value); }

需要使用 std::move:
maybe(T value) : valid_(true)  { new (&value_) T(std::move(value)); }

和其他防御措施:

maybe(T value) : valid_(true)  { ::new ((void*)&value_) T(std::move(value)); }

需要处理operator new重载。

接下来,你需要处理抛出异常的构造函数:

maybe(T value) : maybe()  {
  ::new ((void*)&value_) T(std::move(value));
  valid_ = true;
}

你应该更进一步。
template<class...Args>
void emplace(Args&&...args) {
  if (*this)
    clear();
  ::new( (void*)&value_ ) T( std::forward<Args>(args)... );
  valid_ = true;
}
void clear() {
  if (*this)
  {
    valid_ = false; // set false first in case ~T throws
    value().~T(); 
  }
}

现在我们:
maybe(T value)
: maybe() {
  emplace( std::move(value ) );
}

~maybe()
{
  clear();
}

只需手动调整emplace()clear()中的value_valid_生命周期。

我们还应该拥有有效的noexcept值:

template<class...Args>
void emplace(Args&&...args)
  noexcept(
    noexcept( T( std::forward<Args>(args)... )
    && noexcept( std::declval<maybe&>().clear() )
 )
{
  if (*this)
    clear();
  ::new( (void*)&value_ ) T( std::forward<Args>(args)... );
  valid_ = true;
}
void clear() noexcept(noexcept( std::declval<T&>().~T() ) ) {
  if (*this)
  {
    valid_ = false; // set false first in case ~T throws
    value().~T(); 
  }
}

现在我们:

maybe(T value)
  noexcept( noexcept( std::declval<maybe&>().emplace( std::declval<T&&>() ) ) )
: maybe() {
  emplace( std::move(value ) );
}

~maybe()
  noexcept( noexcept( std::declval<maybe&>().clear() ) )
{
  clear();
}

这只是一个简化的例子(实际上我不是从一个值构建,而是采用const T&),但关于异常等方面的想法非常有用,谢谢。 - gct

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接