【问题标题】:Is there a better way in C++11 to construct classes on the stack在 C++11 中是否有更好的方法在堆栈上构造类
【发布时间】:2014-04-23 12:25:17
【问题描述】:

如果我有两个类 D1 和 D2 都派生自类 Base,并且我想基于一个布尔变量构建一个特定的类,那么有各种众所周知的技术,例如使用工厂或使用智能指针.

例如,

    std::unique_ptr<Base> b;
    if (flag)
    {
            b.reset(new D1());
    }
    else
    {
            b.reset(new D2());
    }

但这使用堆进行分配,这通常很好,但我可以想到避免内存分配的性能影响的时候。

我试过了:

Base b = flag ? D1() : D2();      // doesn’t compile

Base& b = flag ? D1() : D2();     // doesn’t compile

Base&& b = flag ? D1() : D2();    // doesn’t compile

Base&& b = flag ? std::move(D1()) : std::move(D2());   // doesn’t compile

我的意图是选择的 D1 或 D2 在堆栈上构建,并且当 b 超出范围时其生命周期结束。直觉上,我觉得应该有办法做到这一点。

我玩过 lambda 函数,发现这行得通:

Base&& b = [j]()->Base&&{
                 switch (j)
                 {
                 case 0:
                       return std::move(D1());
                 default:
                       return std::move(D2());
                 }
          }();

我不知道为什么它不会遇到与其他不编译的问题相同的问题。 此外,它仅适用于复制成本较低的类,因为尽管我明确要求使用移动,但我认为它仍然调用复制构造函数。但如果我拿走 std::move,我会收到警告!

我觉得这更接近我认为应该可能的,但它仍然存在一些问题:

  • lambda 语法对还没学过的老手不友好 接受了语言的新特性(包括我自己)
  • 前面提到的复制构造函数调用

有更好的方法吗?

【问题讨论】:

  • std::move 只是一个演员表。它实际上并没有移动任何东西。你最终会得到一个悬空引用。
  • 你不能只构造类对象。

标签: c++11 lambda stack allocation lifetime


【解决方案1】:

如果您知道所有类型,则可以使用Boost.Variant,如:

class Manager
{   
    using variant_type = boost::variant<Derived1, Derived2>;

    struct NameVisitor : boost::static_visitor<const char*>
    {   
        template<typename T>
        result_type operator()(T& t) const { return t.name(); }
    };  

public:
    template<typename T>
    explicit Manager(T t) : v_(std::move(t)) {}

    template<typename T>
    Manager& operator=(T t)
    { v_ = std::move(t); return *this; }

    const char* name()
    { return boost::apply_visitor(NameVisitor(), v_); }

private:
        variant_type v_; 

};  

注意:通过使用变体,您不再需要基类或虚函数。

【讨论】:

    【解决方案2】:

    按照您尝试的方式,您将获得一个悬空参考。拥有std::move 只是隐藏了这一点。

    通常我只是构造代码,以便逻辑在一个单独的函数中。也就是说,而不是

    void f(bool flag)
    {
       Base &b = // some magic to choose which derived class to instantiate
    
       // do something with b
    }
    

    我愿意

    void doSomethingWith(Base &b)
    {
       // do something with b
    }
    
    void f(bool flag)
    {
      if (flag) {
        D1 d1;
        doSomethingWith(d1);
      }
      else {
        D2 d2;
        doSomethingWith(d2);
      }
    }
    

    但是,如果这对您不起作用,您可以在类中使用联合来帮助管理它:

    #include <iostream>
    
    using std::cerr;
    
    struct Base {
      virtual ~Base() { }
      virtual const char* name() = 0;
    };
    
    struct Derived1 : Base {
      Derived1() { cerr << "Constructing Derived1\n"; }
      ~Derived1() { cerr << "Destructing Derived1\n"; }
      virtual const char* name() { return "Derived1"; }
    };
    
    struct Derived2 : Base {
      Derived2() { cerr << "Constructing Derived2\n"; }
      ~Derived2() { cerr << "Destructing Derived2\n"; }
      virtual const char* name() { return "Derived2"; }
    };
    
    template <typename B,typename D1,typename D2>
    class Either {
      union D {
        D1 d1;
        D2 d2;
        D() { }
        ~D() { }
      } d;
      bool flag;
    
      public:
        Either(bool flag)
          : flag(flag)
        {
          if (flag) {
            new (&d.d1) D1;
          }
          else {
            new (&d.d2) D2;
          }
        }
    
        ~Either()
        {
          if (flag) {
            d.d1.~D1();
          }
          else {
            d.d2.~D2();
          }
        }
    
    
        B& value()
        {
          if (flag) {
            return d.d1;
          }
          else {
            return d.d2;
          }
        }
    };
    
    static void test(bool flag)
    {
      Either<Base,Derived1,Derived2> either(flag);
    
      Base &b = either.value();
    
      cerr << "name=" << b.name() << "\n";
    }
    
    int main()
    {
      test(true);
      test(false);
    }
    

    给出这个输出:

    构造派生1 名称=派生1 破坏派生1 构造派生2 名称=派生2 破坏派生2

    【讨论】:

    • 您可以将其保留在本地通过将 doSomething 设为 lambda,即 auto doSomething = [](Base&amp;){ ... }; if (flag) ... 将逻辑分解为单独的函数
    【解决方案3】:

    您可以使用std::aligned_storage 确保您有足够的空间在堆栈上分配任何一个。比如:

    // use macros for MAX since std::max is not const-expr
    std::aligned_storage<MAX(sizeof(D1), sizeof(D2)), MAX(alignof(D1), alignof(D2))> storage;
    Base* b = nullptr;
    
    if (flag)
      b = new (&storage) D1();
    else
      b = new (&storage) D2();
    

    您可以为 aligned_storage 创建一个包装器类型,它只需要两种类型,并在两种类型中实现最大的大小/对齐,而无需在使用它的代码中重复自己。如果您需要 C++98 支持,您也可以相当简单地模拟 aligned_storage 的非过度对齐类型。没有过度对齐支持的自定义类型类似于:

    template <typename T1, typename T2>
    class storage
    {
      union
      {
        double d; // to force strictest alignment (on most platforms)
        char b[sizeof(T1) > sizeof(T2) ? sizeof(T1) : sizeof(T2)];
      } u;
    };
    

    如果您愿意,可以提供防止复制/移动的保护。它甚至可以用相对较少的工作变成一个简化的 Boost.Variant。

    请注意,使用这种方法(或其他一些方法),析构函数将在您的类上自动调用,您必须自己调用它们。如果您希望在此处应用 RAII 模式,您可以扩展上面的示例类以将构造期间绑定的删除器函数存储到空间中。

    template <typename T1, typename T2>
    class storage
    {
      using deleter_t = void(*)(void*);
      std::aligned_storage<
        sizeof(T1) > sizeof(T2) ? sizeof(T1) : sizeof(T2),
        alignof(T1) > alignof(T2) ? alignof(T1) : alignof(T2)
      > space;
      deleter_t deleter = nullptr;
    public:
      storage(const storage&) = delete;
      storage& operator=(const storage&) = delete;
      template <typename T, typename ...P>
      T* emplace(P&&... p)
      {
        destroy();
        deleter = [](void* obj){ static_cast<T*>(obj)->~T(); }
        return new (&space) T(std::forward<P>(p)...);
      }
      void destroy()
      {
        if (deleter != nullptr)
        {
          deleter(&space);
          deleter = nullptr;          
        }
      }
    };
    
    // usage:
    storage<D1, D2> s;
    B* b = flag ? s.emplace<D1>() : s.emplace<D2>();
    

    当然,这一切都可以在 C++98 中完成,只是需要做更多的工作(尤其是在模拟 emplace 函数方面)。

    【讨论】:

      【解决方案4】:

      怎么样

      B&&b = flag ? static_cast<B&&>(D1()) : static_cast<B&&>(D2());
      

      【讨论】:

      • b 将保留为悬空引用,因为在该语句之后临时的 D1D2 将被破坏。
      【解决方案5】:

      如果您只需要在引用超出范围时释放它们,您可以实现另一个指向对象(D1D2)的简单类(可能名为DestructorDecorator)。然后你只需要实现~DestructorDecorator 来调用D1D2 的析构函数。

      【讨论】:

        【解决方案6】:

        你还没有提到它,你的flag在编译时是已知的?

        就编译时标志而言,您可以使用模板魔术来处理类的条件构造:

        首先,声明一个模板create_if,它接受两种类型和一个布尔值:

        template <typename T, typename F, bool B> struct create_if {};
        

        第二,将create_if 专门用于truefalse 值:

        template <typename T, typename F> struct create_if<T, F, true> { using type = T; };
        template <typename T, typename F> struct create_if<T, F, false> { using type = F; };
        

        然后,您可以这样做:

        create_if<D1, D2, true>::type da;  // Create D1 instance
        create_if<D1, D2, false>::type db; // Create D2 instance
        

        您可以使用编译时标志或 constexpr 函数更改布尔文字:

        constexpr bool foo(const int i) { return i & 1; }
        create_if<D1, D2, foo(100)>::type dc; // Create D2 instance
        create_if<D1, D2, foo(543)>::type dd; // Create D1 instance
        

        这仅在编译时知道flag 时才有效,希望对您有所帮助。

        Live example.

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2021-01-30
          • 1970-01-01
          • 1970-01-01
          • 2012-12-25
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多