【问题标题】:C++ using declaration for parameter packC++ 使用参数包的声明
【发布时间】:2020-07-17 07:14:15
【问题描述】:

我想定义一个类,它继承自一堆类,但不隐藏这些类的某些特定方法。

想象一下下面的代码:

template<typename... Bases>
class SomeClass : public Bases...
{
public: 
  using Bases::DoSomething...;

  void DoSomething(){
    //this is just another overload
  }
};

现在的问题是,如果只有一个类没有名为 DoSomething 的成员,我会收到错误消息。 我已经尝试过使用宏和 SFINAE 模拟“忽略如果未定义使用”,但要处理所有情况,这变得非常大而且丑陋! 你有什么办法解决这个问题吗?

如果我能定义:“嘿,使用 - 忽略缺失的成员”,那就太好了。

这里有一些示例代码:Godbolt

【问题讨论】:

  • 没有完全充实,但是您可以为您继承的类定义一个包装模板,如果不存在则添加 DoSomething(如果您可以使用应该很容易的 c++20 概念)然后@ 987654325@
  • 不幸的是,这并不容易——你必须处理带有和不带有泛型参数的字段、静态方法和实例方法......此外,这个不存在/人工的“DoSomething”应该是不可调用的跨度>
  • void DoSomething() 放入一个辅助类,并从它与Bases... 一起派生。
  • 然后我收到错误“通过不明确的名称查找找到的成员”请参阅:godbolt.org/z/_oQFJ5

标签: c++ c++17 c++20 using-declaration name-hiding


【解决方案1】:

Jarod42's approach 的问题在于你改变了重载决议的样子——一旦你把所有东西都变成了模板,那么所有东西都是完全匹配的,你就不能再区分多个可行的候选者了:

struct A { void DoSomething(int); };
struct B { void DoSomething(double); };
SomeClass<A, B>().DoSomething(42); // error ambiguous

保留重载决议的唯一方法是使用继承。

这里的关键是完成ecatmur 开始的事情。但是HasDoSomething 看起来像什么?链接中的方法仅在存在单个非重载非模板时才有效。但我们可以做得更好。我们可以使用相同的机制来检测 DoSomething 是否存在,即需要以 using 开头的机制:来自不同范围的名称不会重载。

因此,我们引入了一个新的基类,它有一个永远不会被真正选择的DoSomething——我们通过创建我们自己的明确标签类型来做到这一点,我们是唯一会构建的标签类型。由于没有更好的名字,我将以我的狗命名,它是一只 Westie:

struct westie_tag { explicit westie_tag() = default; };
inline constexpr westie_tag westie{};
template <typename T> struct Fallback { void DoSomething(westie_tag, ...); };

为了更好地衡量它,让它成为可变参数,只是为了让它最少。但其实无所谓。现在,如果我们引入一个新类型,比如:

template <typename T> struct Hybrid : Fallback<T>, T { };

然后,当T 确实DoSomething 重载时,我们可以在混合上调用DoSomething() - 任何类型。那是:

template <typename T, typename=void>
struct HasDoSomething : std::true_type { };
template <typename T>
struct HasDoSomething<T, std::void_t<decltype(std::declval<Hybrid<T>>().DoSomething(westie))>>
    : std::false_type
{ };

请注意,通常在这些特征中,主要特征是 false,而特化特征是 true - 这里反过来。这个答案和 ecatmur 的主要区别在于,回退的重载必须仍然可以以某种方式调用 - 并使用该功能来检查它 - 只是对于用户实际使用的任何类型,它实际上都不会被调用。

以这种方式检查可以让我们正确地检测到:

struct C {
    void DoSomething(int);
    void DoSomething(int, int);
};

确实满足HasDoSomething

然后我们使用 ecatmur 展示的相同方法:

template <typename T>
using pick_base = std::conditional_t<
    HasDoSomething<T>::value,
    T,
    Fallback<T>>;

template<typename... Bases>
class SomeClass : public Fallback<Bases>..., public Bases...
{
public: 
  using pick_base<Bases>::DoSomething...;

  void DoSomething();
};

无论BasesDoSomething 重载是什么样的,这都能正常工作,并且在我提到的第一种情况下正确执行重载解析。

Demo

【讨论】:

  • 感谢您的出色解决方案 - 但我仍然对此不满意。如果“A”有一个私有的“DoSomething”,它不会编译。这使得使用对我来说毫无用处......(见godbolt.org/z/ba5aM6
  • @BerndBaumanns 有A 朋友SomeClass。总得在某处有所付出。
  • 快速响应和好主意!但不幸的是,我无法更改“A”类。
  • @BerndBaumanns 具有相同名称的私有和公共访问权限会破坏 using-declarations,并且通常是不好的做法。解决这个问题的唯一方法是为 A 编写一个包装器,将公共成员函数转发到。
  • 是否有在 C++ 中处理私有/受保护成员的最佳实践?我通常不希望私有函数隐藏继承的公共/受保护函数。
【解决方案2】:

有条件地使用后备怎么样?

为每个方法创建不可调用的实现:

template<class>
struct Fallback {
    template<class..., class> void DoSomething();
};

为每个基类从Fallback 继承一次:

class SomeClass : private Fallback<Bases>..., public Bases...

然后有条件地从基类或其各自的后备中拉入每个方法:

using std::conditional_t<HasDoSomething<Bases>::value, Bases, Fallback<Bases>>::DoSomething...;

Example.

【讨论】:

  • 尽管Demo,您的特征还不够好,例如因重载而失败。
  • @Jarod42 当然,我将HasDoSomething 的实现留给读者作为练习。有众所周知的技术来处理重载,参见例如stackoverflow.com/a/39750372/567292
【解决方案3】:

您可以添加通过转发而不是using 来处理基本案例的包装器:

template <typename T>
struct Wrapper : T
{
    template <typename ... Ts, typename Base = T>
    auto DoSomething(Ts&&... args) const
    -> decltype(Base::DoSomething(std::forward<Ts>(args)...))
    {
        return Base::DoSomething(std::forward<Ts>(args)...);
    }

    template <typename ... Ts, typename Base = T>
    auto DoSomething(Ts&&... args)
    -> decltype(Base::DoSomething(std::forward<Ts>(args)...))
    {
        return Base::DoSomething(std::forward<Ts>(args)...);
    }

    // You might fix missing noexcept specification
    // You might add missing combination volatile/reference/C-elipsis version.
    // And also special template versions with non deducible template parameter...
};

template <typename... Bases>
class SomeClass : public Wrapper<Bases>...
{
public: 
  using Wrapper<Bases>::DoSomething...; // All wrappers have those methods,
                                        // even if SFINAEd

  void DoSomething(){ /*..*/ }
};

Demo

正如 Barry 所指出的,随着重载分辨率的改变,还有其他缺点,使得一些调用模棱两可......

注意:我提出了这个解决方案,因为我不知道如何创建正确的特征来在所有情况下检测DoSomething 的存在(主要是重载问题)。 巴里解决了这个问题,所以你有更好的选择。

【讨论】:

  • 您的解决方案似乎有效,但它错过了(正如您在 cmets 中已经提到的)某些情况(静态与非静态,模板参数......)。在我做类似的事情之前 - 但不是包装器基类,我编写了一个带有一堆委托函数和 SFINAE 的宏,因为使用这些转发方法你真的不需要使用。
  • 处理静态/非静态。唯一不能完全处理并需要知道Bases信息的部分是不可演绎的模板。这并不常见(std::get&lt;I&gt;(..)。即使对于这些,您也可以在Wrapper 中手动添加重载(确实不理想)。顺便说一句,即使模板函数也可能与using 有问题。
  • 这两种方法都不是静态的 - 所以它们没有被处理,或者?
  • 我的意思是包装方法可以调用静态方法,因为 Base::f() 可以解释为两种方式,静态调用或基类调用 (this-&gt;Base::f()) Demo 但实际上,方法不再静态在SomeClass/Wrapper.
【解决方案4】:

只要您愿意使用别名模板来命名您的类,您就可以在没有额外基类的情况下实现这一点。诀窍是根据谓词将模板参数分隔成两个包:

#include<type_traits>

template<class,class> struct cons;  // not defined
template<class ...TT> struct pack;  // not defined

namespace detail {
  template<template<class> class,class,class,class>
  struct sift;
  template<template<class> class P,class ...TT,class ...FF>
  struct sift<P,pack<>,pack<TT...>,pack<FF...>>
  {using type=cons<pack<TT...>,pack<FF...>>;};
  template<template<class> class P,class I,class ...II,
           class ...TT,class ...FF>
  struct sift<P,pack<I,II...>,pack<TT...>,pack<FF...>> :
    sift<P,pack<II...>,
         std::conditional_t<P<I>::value,pack<TT...,I>,pack<TT...>>,
         std::conditional_t<P<I>::value,pack<FF...>,pack<FF...,I>>> {};

  template<class,class=void> struct has_something : std::false_type {};
  template<class T>
  struct has_something<T,decltype(void(&T::DoSomething))> :
    std::true_type {};
}

template<template<class> class P,class ...TT>
using sift_t=typename detail::sift<P,pack<TT...>,pack<>,pack<>>::type;

然后分解结果并从各个类继承:

template<class> struct C;
template<class ...MM,class ...OO>  // have Method, Others
struct C<cons<pack<MM...>,pack<OO...>>> : MM...,OO... {
  using MM::DoSomething...;
  void DoSomething();
};

template<class T> using has_something=detail::has_something<T>;

template<class ...TT> using C_for=C<sift_t<has_something,TT...>>;

请注意,为简单起见,此处的 has_something 仅支持非重载方法(每个基类);请参阅 Barry 对此的概括回答。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-03-28
    • 1970-01-01
    • 1970-01-01
    • 2016-01-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-07-21
    相关资源
    最近更新 更多