我可以将一个类与模板中的另一个类关联(使用 C++17 变体)吗?

Posted

技术标签:

【中文标题】我可以将一个类与模板中的另一个类关联(使用 C++17 变体)吗?【英文标题】:Can I associate one class with another from a template (using C++17 variant)? 【发布时间】:2020-11-16 23:08:14 【问题描述】:

我有一些代码接受一种类型的对象并根据第一种类型创建另一种类型的对象。 (类型之间存在 1->1 的关系。)我最初使用哈希表 (unordered_map<>) 和基于第一个对象类型的键来关联第二个对象的创建函数。但是当我更多地了解自上次全职 C++ 以来引入的 C++ 特性时,我发现了std::variant<>。 我已成功将实现转换为使用此 C++17 功能。但是,剩下的一块仍然有点麻烦。该设计在实例化第二个类的对象之前调用第二个类的静态成员函数以验证第一个对象的内容。为了立即处理这个问题,我使用了一个访问者结构,其中每个输入类型都重载了函数运算符。 我想知道是否有某种方法可以使用模板进行关联,而不是只复制类型不同的代码? 我已经尝试查看std::variant<> 的工作方式,并且我看到了可以使用.index() 获得类型索引的位置。我可以看到如何基于索引来实例化对象,如果我使用对象类型创建第二个std::variant<>,我可能会使用该索引。但是,如您所见,在验证参数之前,我不想实例化对象。执行此操作的函数是静态的,我看不到一种方法可以将 parms 类型与对象类型关联起来,从而让我可以进行静态调用。 (我也意识到这两个访问者结构可以在下面的代码中组合,但是在真实的代码中,创建更长更复杂,我宁愿在每个重载中都没有它的副本。)

struct Type1Parms ;
struct Type2Parms ;
struct Type3Parms ;
...

struct TypeBase ;
struct Type1 : public TypeBase

    static bool ValidateParms(const Type1Parms&);
    Type1(const Type1Parms&);
;
struct Type2 : public TypeBase

    static bool ValidateParms(const Type2Parms&);
    Type2(const Type2Parms&);
;
struct Type3 : public TypeBase

    static bool ValidateParms(const Type3Parms&);
    Type3(const Type3Parms&);
;
...

struct ValidateParmsVisitor

    bool operator()(const Type1Parms& parms)
    
        return Type1::ValidateParms(parms);
    
    bool operator()(const Type2Parms& parms)
    
        return Type2::ValidateParms(parms);
    
    bool operator()(const Type3Parms& parms)
    
        return Type3::ValidateParms(parms);
    
...
;

using TypeParms = std::variant<Type1Parms, Type2Parms, Type3Parms, ...>;

struct CreateObjectVisitor

    std::unique_ptr<TypeBase> operator()(const Type1Parms& parms)
    
        return std::make_unique<Type1>(parms);
    
    std::unique_ptr<TypeBase> operator()(const Type2Parms& parms)
    
        return std::make_unique<Type2>(parms);
    
    std::unique_ptr<TypeBase> operator()(const Type3Parms& parms)
    
        return std::make_unique<Type3>(parms);
    
...
;

template<typename TParms>
std::unique_ptr<TypeBase> CreateType(const TParms& parms)

    unique_ptr<TypeBase> obj;
    if (visit(ValidateParmsVisitor, parms))
        obj = visit(CreateObjectVisitor, parms);
    return std::move(obj);

有没有办法建立这种关联,尤其是作为可以与静态成员函数调用一起使用的类型? 编辑:我应该解释一下,这是一个更大的项目的一部分,还有许多其他设计标准塑造了它的设计。 例如,这是针对客户端接口的,其中 API 旨在尽可能简单地表达。客户端只有对 parms 结构的可见性(通过标头)和一个接受 parms 并返回包含上述对象的对象的函数。最初的设计确实有一个用于 parms 的基础结构,显然必须在公共标题中。但是,这意味着客户端可以从基类本身继承并将其传递给对象创建函数,或者从可接受的结构继承。为了避免段错误,这需要添加运行时检查以确保类型是可接受的,这主要由散列设计处理——尽管它不是那么简单。当我删除散列设计时,我也失去了这种类型验证的方法,但我意识到这将被使用variant&lt;&gt; 的编译时检查所取代,处理自定义结构(现在没有要检查的基础)。我还了解了处理继承问题的 final 关键字的 C++ 版本。 此外,虽然上面的代码没有显示,但 parms 结构包含多个成员,ValidateParms() 函数实际上尝试验证值和组合是否有效。

【问题讨论】:

I also realize that these two visitor structures can be combined in the code below, but in the real code, the creation is longer and more complicated, and I would rather not have copies of it in each overload. 能否将验证移到创建访问者中,但将创建的返回值设为可选变量或将空值添加到创建的对象变量中? 是的,但正如我提到的(以及你引用的),调用 validate 和 create 的函数有更多代码,包括两个调用之间的代码。这是一个简化的例子,让问题更清楚。 是的,在我看来,将通用创建代码移入函数或其他东西比做类型特征等更容易,但这取决于实际代码。 【参考方案1】:

您可以为关联创建特征:

template <typename T> struct from_param;

template <> struct from_param<Type1Parms>  using type = Type1; ;
template <> struct from_param<Type2Parms>  using type = Type2; ;
template <> struct from_param<Type3Parms>  using type = Type3; ;

那么,你可能会这样做

using TypeParms = std::variant<Type1Parms, Type2Parms, Type3Parms>;

std::unique_ptr<TypeBase> CreateType(const TypeParms& parms)

    if (std::visit([](const auto& param)
            return from_param<std::decay_t<decltype(param)>>::type::ValidateParms(parms);
        , parms))
    
        return std::visit([](const auto& param) -> std::unique_ptr<TypeBase> 
            return std::make_unique<typename from_param<std::decay_t<decltype(param)>>::type>(parms);
        , parms);
    
    return nullptr;

Demo

或者没有变体,如果你用正确的类型调用:

template <typename T>
auto CreateType(const T& parms)

    if (from_param<T>::type::ValidateParms(parms))
    
        return std::make_unique<typename from_param<T>::type>(parms);
    
    return nullptr;

【讨论】:

这正是我希望看到的(谢谢!)。但是我无法编译它。首先,必须将参数捕获添加到 lambda。现在出现错误:无效使用不完整类型“struct from_param”。 (我试过 ...from_param...,但这没有帮助。)我仍在努力解决。 如前所述,这在实际代码中要复杂一些。有几个variant&lt;&gt;s 正在使用中,1 目前只有一种类型。但是对于更多类型的variant&lt;&gt;s,也会出现同样的错误。很明显,编译器没有使用专门的模板。我尝试更改 lambda 的参数类型:visit([&amp;parms](auto p) return from_param...,它似乎确实使用了专业化,但错误更改为:no matching function for call to ‘Type1::ValidateParms(const variant&lt;Type1Parms&gt;&amp;)’note: candidate: static bool Type1::ValidateParms(const Type1Parms&amp;) 您能否提供有关 coliru 或 Godbold 或任何其他在线编译器的 MCVE? 在将哈希 (unordered_map&lt;&gt;) 更改为 variant&lt;&gt; 时,我一直在使用完整的源代码,并将您的代码也放在那里。但是为了获得更好的外观和/或分享,我将不得不将其提炼成一个独立的应用程序。我们必须使用的编译器是 7.3.0,所以我想知道这是否可能是编译器问题。否则就是键盘和地之间短路了。 B-| 我已经提炼了这一点,并且惊讶地发现问题仍然存在于较新的编译器中。起点是coliru.stacked-crooked.com/a/ebe350ef54925d3b。如上所述,为了查看是否可以帮助编译器使用专用模板,我修改了第 131 和 137 行以删除 const & 引用,从而改变了错误。蒸馏错误是不同的(无法转换与没有匹配函数),但仍显示无法从 variant&lt;&gt; 转换为其一种类型。我尝试了其他一些方法,但没有任何方法可以让我找到解决方案。【参考方案2】:

有一个很简单的方法,一组重载函数:

unique_ptr<TypeBase> CreateType(Type1Params const& params)

    return make_unique<Type1>(params);

unique_ptr<TypeBase> CreateType(Type2Params const& params)

    return make_unique<Type2>(params);

unique_ptr<TypeBase> CreateType(Type3Params const& params)

    return make_unique<Type3>(params);

注意事项:

您可以添加另一个重载来捕获其他参数,然后返回 null,但我认为编译时错误会更好。 您也可以使用模板函数和特化,但这种方式可能需要很少的输入来保证安全。

【讨论】:

以上是关于我可以将一个类与模板中的另一个类关联(使用 C++17 变体)吗?的主要内容,如果未能解决你的问题,请参考以下文章

C++中模板类与模板方法的使用

模板类与抽象类

C++中类与函数的模板类型推导?

将派生类中的对象插入到所述对象的另一个实例中(使用 vector.insert 函数)C++

模板类与类模板函数模板与模板函数等的区别

如何将 A 类与 B 类关联,并从 A 类中的方法返回对 B 类的引用?