【问题标题】:C++ templates: conditionally enabled member functionC++ 模板:有条件启用的成员函数
【发布时间】:2014-10-29 14:40:02
【问题描述】:

我正在创建一个非常小的 C++ 项目,我想根据自己的需要创建一个简单的矢量类。 std::vector 模板类不行。当向量类由chars(即vector<char>)组成时,我希望它能够与std::string 进行比较。经过一番折腾,我编写了既能编译又能做我想做的代码。见下文:

#include <string>
#include <stdlib.h>
#include <string.h>

template <typename ElementType>
class WorkingSimpleVector {
 public:
    const ElementType * elements_;
    size_t count_;

    // ...

    template <typename ET = ElementType>
    inline typename std::enable_if<std::is_same<ET, char>::value && std::is_same<ElementType, char>::value, bool>::type
    operator==(const std::string & other) const {
        if (count_ == other.length())
        {
            return memcmp(elements_, other.c_str(), other.length()) == 0;
        }
        return false;
    }
};

template <typename ElementType>
class NotWorkingSimpleVector {
 public:
    const ElementType * elements_;
    size_t count_;

    // ...

    inline typename std::enable_if<std::is_same<ElementType, char>::value, bool>::type
    operator==(const std::string & other) const {
        if (count_ == other.length())
        {
            return memcmp(elements_, other.c_str(), other.length()) == 0;
        }
        return false;
    }
};

int main(int argc, char ** argv) {
    // All of the following declarations are legal.
    WorkingSimpleVector<char> wsv;
    NotWorkingSimpleVector<char> nwsv;
    WorkingSimpleVector<int> wsv2;
    std::string s("abc");

    // But this one fails: error: no type named ‘type’ in ‘struct std::enable_if<false, bool>’
    NotWorkingSimpleVector<int> nwsv2;

    (wsv == s);  // LEGAL (wanted behaviour)
    (nwsv == s);  // LEGAL (wanted behaviour)
    // (wsv2 == s);  // ILLEGAL (wanted behaviour)
    // (nwsv2 == s);  // ??? (unwanted behaviour)
}

我相信我明白为什么会发生错误:编译器为NotWorkingSimpleVector&lt;int&gt; 创建了类定义,然后我的operator== 函数的返回类型变为:

std::enable_if<std::is_same<int, char>::value, bool>::type

然后变成:

std::enable_if<false, bool>::type

然后产生错误:std::enable_if&lt;false, bool&gt; 中没有type 成员,这确实是enable_if 模板的全部要点。

我有两个问题。

  1. 为什么 SFINAE 不会像我想要的那样简单地为 NotWorkingSimpleVector&lt;int&gt; 禁用 operator== 的定义?这是否有一些兼容性原因?我还缺少其他用例吗?这种行为是否存在合理的反驳论点?
  2. 为什么第一类 (WorkingSimpleVector) 有效?在我看来,编译器“保留判断”:由于尚未定义“ET”参数,因此它放弃了判断operator== 是否存在的尝试。我们是否依赖编译器“缺乏洞察力”来允许这种有条件启用的功能(即使 C++ 规范可以接受这种“缺乏洞察力”)?

【问题讨论】:

标签: c++ templates c++11 enable-if


【解决方案1】:

SFINAE 在模板函数中工作。在模板类型替换的上下文中,替换失败在替换的直接上下文中不是错误,而是算作替换失败。

但是请注意,必须有一个有效的替换,否则您的程序格式不正确,不需要诊断。我认为存在这种情况是为了将来可以将进一步的“更具侵入性”或完整的检查添加到检查模板函数有效性的语言中。只要所述检查实际上是检查模板是否可以使用某种类型进行实例化,它就会成为有效检查,但它可能会破坏期望没有有效替换的模板有效的代码,如果这有意义的话。如果没有模板类型可以传递给可以让程序编译的operator== 函数,这可能会使您的原始解决方案成为格式错误的程序。

在第二种情况下,没有替代上下文,因此 SFINAE 不适用。没有替代失败。

最后我查看了传入的 Concepts 提案,您可以将 requires 子句添加到模板对象中的方法,这些方法依赖于对象的模板参数,并且在失败时,该方法将不会被视为重载解决方案。这实际上就是你想要的。

在当前标准下,没有符合标准的方法来执行此操作。第一次尝试是人们通常会做的尝试,它确实可以编译,但它在技术上违反了标准(但不需要对故障进行诊断)。

我想出的符合标准的方法可以做你想做的事:

如果条件失败,将方法的参数之一更改为对从未完成的类型的引用。方法的主体如果不被调用就永远不会被实例化,并且这种技术会阻止它被调用。

使用 CRTP 基类助手,该助手使用 SFINAE 根据任意条件包含/排除方法。

template <class D, class ET, class=void>
struct equal_string_helper {};

template <class D, class ET>
struct equal_string_helper<D,ET,typename std::enable_if<std::is_same<ET, char>::value>::type> {
  D const* self() const { return static_cast<D const*>(this); }
  bool operator==(const std::string & other) const {
    if (self()->count_ == other.length())
    {
        return memcmp(self()->elements_, other.c_str(), other.length()) == 0;
    }
    return false;
  }
};

我们在哪里这样做:

template <typename ElementType>
class WorkingSimpleVector:equal_string_helper<WorkingSimpleVector,ElementType>

如果我们选择,我们可以从 CRTP 实现中重构条件机制:

template<bool, template<class...>class X, class...>
struct conditional_apply_t {
  struct type {};
};

template<template<class...>class X, class...Ts>
struct conditional_apply_t<true, X, Ts...> {
  using type = X<Ts...>;
};
template<bool test, template<class...>class X, class...Ts>
using conditional_apply=typename conditional_apply_t<test, X, Ts...>::type;

然后我们拆分出没有条件代码的CRTP实现:

template <class D>
struct equal_string_helper_t {
  D const* self() const { return static_cast<D const*>(this); }
  bool operator==(const std::string & other) const {
    if (self()->count_ == other.length())
    {
        return memcmp(self()->elements_, other.c_str(), other.length()) == 0;
    }
    return false;
  }
};

然后将它们连接起来:

template<class D, class ET>
using equal_string_helper=conditional_apply<std::is_same<ET,char>::value, equal_string_helper_t, D>;

我们使用它:

template <typename ElementType>
class WorkingSimpleVector: equal_string_helper<WorkingSimpleVector<ElementType>,ElementType>

在使用时看起来相同。但是背后的机器被重构了,所以,奖金?

【讨论】:

  • 如果第二个equal_string_helper 是类模板特化,那么它需要是template &lt;class D, class ET&gt; struct equal_string_helper&lt;D, ET, typename std::enable_if&lt;std::is_same&lt;ET, char&gt;::value&gt;::type&gt; {...
  • 如果我们把&amp;&amp; std::is_same&lt;ElementType, char&gt;::value去掉,OP中的第一个版本有什么问题?
  • @T.C.技术上没什么!即使ET 不是char(一旦删除该子句),它也是格式正确的实例。通常情况并非如此。我没发现!
  • @Yakk 我在您的解决方案class WorkingSimpleVector: equal_string_helper&lt;WorkingSimpleVector,ElementType&gt; -> class WorkingSimpleVector: equal_string_helper&lt;WorkingSimpleVector&lt;ElementType&gt;,ElementType&gt; 中发现了一个错误。我还对您的解决方案进行了一些改进(IMO)并将它们添加到问题中。如果你有时间,请告诉我你的想法!
  • @astupidnoob 当然。请注意,您的版本仍然会创建相等的字符串助手并丢弃它:立即上下文中的替换失败可能会触发 SFINAE,如果不是立即发生硬错误。但在这种情况下不是问题。用std::conditional替换conditional_apply:你的_apply版本已经写好了。
【解决方案2】:

模板operator== 基本上使它无法调用。你必须明确地这样做:

myvec.operator==<char>(str);

最简单的解决方案可能只是添加一个非成员函数:

bool operator==(const WorkingVector<char>& vec, const std::string& s);
bool operator==(const std::string& s, const WorkingVector<char>& vec);

要启用 SFINAE 并将其保留为成员函数,您必须将您的类型转发给其他内容:

bool operator==(const std::string& s) const {
    return is_equal<ElementType>(s);
}

template <typename T> // sure, T == ET, but you need it in this context
                      // in order for SFINAE to apply
typename std::enable_if<std::is_same<T, char>::value, bool>::type
is_equal(const std::string& s) {
    // stuff
}

【讨论】:

    【解决方案3】:

    简单回答

    这个答案与 Yakk 的答案相似,但不是很有用(Yakk 的答案支持任意 if 表达式)。但是,它更简单,更容易理解。

    template <typename ThisClass, typename ElementType>
    class WorkingSimpleVector_Base {
    
    };
    
    template <typename ThisClass>
    class WorkingSimpleVector_Base<ThisClass, char> {
    private:
        ThisClass * me() { return static_cast<ThisClass*>(this); };
        const ThisClass * me() const { return static_cast<const ThisClass*>(this); };
    public:
        bool operator==(const std::string & other) const {
            if (me()->count_ == other.length())
            {
                return memcmp(me()->elements_, other.c_str(), other.length()) == 0;
            }
            return false;
        }
    };
    
    template <typename ElementType>
    class WorkingSimpleVector : public WorkingSimpleVector_Base<WorkingSimpleVector<ElementType>, ElementType> {
     public:
        const ElementType * elements_;
        size_t count_;
    };
    

    这是通过对我们想要的“if 语句”使用模板特化来实现的。我们以WorkingSimpleVector_Base 为基础,它只包含ElementType 值为char 的函数(WorkingSimpleVector_Base 的第二个定义)。否则,它根本没有任何功能(WorkingSimpleVector_Base 的第一个定义)。 ThisClass 参数使它成为"CRTP" (Curiously Recurring Template Pattern)。它允许模板通过使用me() 函数来访问子类的字段。请记住,模板与任何其他类没有什么不同,因此它无法访问私有成员(除非子类将其声明为friend)。


    Yakk 回答的修改版解释

    他/她声明的第一件事是一个帮助模板,它为我们完成了整个条件声明:

    template<bool, template<class...>class X, class...>
    struct conditional_apply_t {
      struct type {};
    };
    
    template<template<class...>class X, class...Ts>
    struct conditional_apply_t<true, X, Ts...> {
      using type = X<Ts...>;
    };
    
    template<bool test, template<class...>class X, class...Ts>
    using conditional_apply=typename conditional_apply_t<test, X, Ts...>::type;
    

    可变参数模板很可怕,我认为在这种情况下可以将其删除。让我们将其简化为:

    template<bool, class X>
    struct conditional_apply_t {
      struct type {};
    };
    
    template<class X>
    struct conditional_apply_t<true, X> {
      using type = X;
    };
    
    template<bool test, class X>
    using conditional_apply=typename conditional_apply_t<test, X>::type;
    

    如果条件test 不是true,则conditional_apply_ttype 类型为空结构(参见conditional_apply_t 的第一个定义)。如果为真,那么type 类型就是X 的值。 conditional_apply 的定义只是消除了我们每次使用此构造时都需要在 conditional_apply_t&lt;...&gt; 末尾写 ::type 的需要。

    接下来,我们定义一个实现我们想要的行为的模板

    template <class D>
    struct equal_string_helper_t {
      D const* self() const { return static_cast<D const*>(this); }
      bool operator==(const std::string & other) const {
        if (self()->count_ == other.length())
        {
            return memcmp(self()->elements_, other.c_str(), other.length()) == 0;
        }
        return false;
      }
    };
    

    在这种情况下,D 参数为我们提供了"CRTP" (Curiously Recurring Template Pattern)。请参阅上面的“简单答案”,详细了解为什么这很重要。

    接下来,我们声明一个仅在满足条件时才具有此operator== 函数的类型:

    template<class D, class ET>
    using equal_string_helper=conditional_apply<std::is_same<ET,char>::value, equal_string_helper_t<D>>;
    

    所以,equal_string_helper&lt;D,ET&gt; 类型是:

    • ET != char 时为空结构
    • equal_string_helper_t&lt;D&gt;ET == char

    最后,我们可以使用以下代码创建我们想要的类:

    template <typename ElementType>
    class WorkingSimpleVector : public equal_string_helper<WorkingSimpleVector<ElementType>, ElementType> {
     public:
        const ElementType * elements_;
        size_t count_;
    };
    

    按要求工作。

    【讨论】:

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