auto x = make_x(...)和这个

时间:2014-02-03 19:11:15

标签: c++ c++11

我有一个类模板S<T>,因为模板参数有时候是硬写的,所以我也有一个辅助函数makeS(...)来推导出模板参数。

现在的问题是S的构造函数有一个“副作用”:它将自己添加到map,然后将其用于迭代S的所有实例。但这有效地使S<T> s{...};auto s = makeS(...);非常不同(如果不使用RVO)。

这是一些代码,希望能够展示我正在尝试做的事情。 (注意:在实际程序中,S包含多个模板参数,所有内容都将在makeS中推断出来)

#include <cassert>
#include <iostream>
#include <map>
#include <string>
#include <utility>

using namespace std;

struct Base
{
    virtual ~Base() {}

    virtual void f() const = 0;
};

static map<string, Base*> Map;

template <typename T>
struct S : Base
{
    T Func;
    Base* This;

    S(string const& name, T func) : Func(std::move(func)), This(this)
    {
        //
        // Automatically add this struct to Map...
        //

        Map.insert({name, this});
    }

    virtual void f() const override { Func(); }
};

template <typename T>
S<T> makeS(std::string const& name, T func)
{
    return S<T>(name, std::move(func));
}

void func1()
{
    std::cout << "func1\n";
}

int main()
{
    struct Func2
    {
        void operator ()() const {
            std::cout << "func2\n";
        }
    };

    //
    // This is not possible:
    //
    // S< ??? > s("s", [](){});
    //
    // This would be ok:
    //
    // auto F = [](){};
    // S<decltype(F)> s("s", F);
    //

    auto s1 = makeS("s1", func1);
    auto s2 = makeS("s2", Func2());
    auto s3 = makeS("s3", [](){ std::cout << "func3\n"; });

    //
    // Manually adding s1,... to the Map is ok, but that's what
    // I want to avoid...
    //
    // Map.insert({"s1", &s1});
    // ...
    //

    assert(&s1 == s1.This);
    assert(&s2 == s2.This);
    assert(&s3 == s3.This);

    for (auto&& I : Map)
    {
        I.second->f();
    }
}

根据我的理解,如果在auto s1 = makeS(...)等中使用RVO,地图将只包含有效指针,但这不能保证。

有没有办法推断出模板参数,同时又无需手动注册s1,...

3 个答案:

答案 0 :(得分:1)

您的基本问题是您未能实现3的规则。如果您的析构函数需要非平凡的行为(如果您在构造函数中注册自己,就是这种情况),您必须实现或阻止赋值和复制构造(和/或move-assign和move-construct)。

在这种情况下,我们可以实现move - 构造并阻止move - assign,并且隐式阻止复制构造和复制赋值。

首先,将name添加到S。然后实现move构造函数。

template <typename T>
struct S : Base
{
  std::string Name;
  T Func;
  Base* This; // ?? why ?? this looks both dangerous and useless at the same time!
  S( S&& s ): Name(std::move(s.Name)), Func(std::move(s.Func)), This(this) {
    s.clear(); // technically `move` need not clear.
    map[Name] = this; // overwrite
  }
  S& operator=(S&& s) = delete; // or implement it

现在您的对象已move能够,move时它会更新Map。我认为~S应该从Map取消注册 - 检测您的name是否为空(并在构造时声明您获得非空名称),如果不是取消注册(因为你已经move来自哪里)。

现在move - construct和elided-construct具有相同的语义。 RVO故障导致一些低效率,但没有逻辑故障。此外,您的类型现在move能够使用,这往往非常有用。

答案 1 :(得分:0)

如果您需要维护对象标识,请使用std::unique_ptr

template <typename T>
std::unique_ptr<S<T>> makeS(std::string const& name, T func)
{
    return { new S<T>(name, std::move(func)) };
}

现在将指针从一个地方移动到另一个地方不会移动对象;保存在地图中的指针将保持有效。

答案 2 :(得分:0)

我对改进代码的建议:
1)摆脱构造函数中的副作用。仅在工厂方法中创建对象(在代码中为makeS;您可以使其成为S的静态成员函数)并在该方法中注册S个对象。要以不同方式禁用对象创建,请将构造函数设置为私有。

2)禁用S个对象复制/移动并处理对象,例如仅shared_ptr / unique_ptr<S>。当您禁用复制/移动时,当地图包含指向无效对象的指针时,您可以避免此问题,现在您不需要依赖RVO。

3)使用std::function<void()>代替T Func;。在这种情况下,您的类不需要是模板类(或者它将具有更少的模板参数)。这将简化您的代码。