C ++模板:有条件启用的成员函数 [英] C++ templates: conditionally enabled member function

查看:200
本文介绍了C ++模板:有条件启用的成员函数的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我创建了一个非常小的C ++项目,我想为我自己的需要创建一个简单的向量类。 std :: vector 模板类不会。当向量类由 char s(即 vector )组成时,我希望它能够与 std :: string 进行比较。经过一点乱七八糟,我写的代码,既编译,做我想要的。如下:

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

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

// ...

模板< 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
运算符==(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){
//以下所有声明都是合法的。
WorkingSimpleVector< char> wsv;
NotWorkingSimpleVector< char> nwsv;
WorkingSimpleVector< int> wsv2;
std :: string s(abc);

//但是这一个失败:错误:没有在'struct std :: enable_if< false,bool>'中命名为'type'的类型
NotWorkingSimpleVector< int& nwsv2;

(wsv = s); // LEGAL(wanted behavior)
(nwsv == s); // LEGAL(wanted behavior)
//(wsv2 == s); // ILLEGAL(想要的行为)
//(nwsv2 == s); // ??? (不需要的行为)
}



我相信我理解错误发生的原因:创建 NotWorkingSimpleVector< int> 的类定义,然后我的操作符== 函数的返回类型变为: / p>

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

这将成为:

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

没有类型成员 std :: enable_if< false,bool> ,这确实是 enable_if 模板。



我有两个问题。


  1. 为什么SFINAE不能简单地禁用 operator == code> NotWorkingSimpleVector< int> ,就像我想要的?这是有一些兼容性原因吗?有没有其他用例我缺少; ( WorkingSimpleVector )为什么工作?

  2. 为什么第一个类在我看来,编译器'保留判断':由于'ET'参数尚未定义,它放弃试图告诉是否运算符== 可以存在。我们是否依赖于编译器缺乏洞察力来允许这种条件启用的函数(即使这种缺乏洞察力是C ++规范可接受的)?

谢谢。






简单回答



这个答案与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奇怪的循环模板模式)。它允许模板通过使用 me()函数来访问子类的字段。记住,模板与任何其他类没有什么区别,所以它不能访问私有成员(除非子类声明为 friend )。






修改后的Yakk答案解释



他/她声明是一个帮助模板,它为我们执行这个完整的条件声明:

 模板< 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;

conditional_apply_t test 不是 true ,请键入 type是一个空结构 conditional_apply_t 的定义)。如果为真,则类型类型是 X 的值。 conditional_apply 的定义只是消除了我们在 :: type 的需要>



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

/ p>

 模板< 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(好奇地重复模板模式)



接下来,我们声明一个只有这个运算符的类型== 函数如果满足条件:

 模板< class D,class ET& 
using equal_string_helper = conditional_apply< std :: is_same< ET,char> :: value,equal_string_helper_t< D>>

所以, equal_string_helper< D,ET> type is:




  • ET!= char

  • equal_string_helper_t< D> ET == char



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

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

可按要求工作。

解决方案

SFINAE在模板函数中工作。在模板类型替换的上下文中,替换中的替换失败在立即上下文中不是错误,而是被视为替换失败。



但请注意,必须是有效的替代品,或者您的计划未成功,无需诊断。我假定这个条件存在,以便将来可以向将来检查模板函数的有效性的语言添加进一步的更具侵入性或完整的检查。只要所述检查实际上检查模板可以用某种类型来实例化,它就变成有效检查,但是它可能破坏期望没有有效替换的模板是有效的代码,如果这是有意义的。如果没有模板类型,你可以传递给让程序编译的运算符== 函数,这可能会使你的原始解决方案成为一个错误的程序。



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



最后,我查看了传入的Concepts提案,可以在依赖于模板参数的模板对象中添加require子句对象,并且在失败时,该方法将不被考虑用于重载分辨率。这实际上是你想要的。



在当前标准下没有符合标准的方法。第一次尝试是人们通常可以做的,它会编译,但它在技术上违反标准(但不需要诊断失败)。



符合我的标准的方式,我已经想出来做你想要的:



将方法的一个参数更改为未完成类型的引用,如果你的条件失败。



使用一个使用SFINAE的CRTP基类辅助器来包含/排除

 模板< 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实现,不带条件代码:

 模板< 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>

但是后面的机制是重构的,所以,奖金?


I'm creating a very small C++ project, and I'd like to create a simple vector class for my own needs. The std::vector template class will not do. When the vector class is comprised of chars (i.e. vector<char>), I'd like it to be able to be compared to a std::string. After a bit of messing around, I wrote code that both compiles and does what I want. See below:

#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)
}

I believe I understand why the error is occurring: The compiler creates the class definition for NotWorkingSimpleVector<int>, and then the return type of my operator== function becomes:

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

which then becomes:

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

which then yields an error: there is no type member of std::enable_if<false, bool>, which is indeed the entire point of the enable_if template.

I have two questions.

  1. Why won't SFINAE simply disable the definition of operator== for NotWorkingSimpleVector<int>, like I want it to? Is there some compatibility reason for this? Are there other use-cases I'm missing; does a reasonable counter-argument for this behaviour exist?
  2. Why does the first class (WorkingSimpleVector) work? It seems to me that the compiler 'reserves judgement': Since the 'ET' parameter is not yet defined, it gives up trying to tell if operator== can exist. Are we relying on the compilers 'lack of insight' to allow this kind of conditionally enabled function (even if this 'lack of insight' is acceptable by the C++ specification)?

Thank you.


Simple answer

This answer bares parallels to Yakk's answer but is not quite as useful (Yakk's one supports arbitrary if-expressions). It is, however, quite a bit simpler and easier to understand.

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_;
};

This works by utilising template specialization for the 'if statements' we want. We base the class off of WorkingSimpleVector_Base, which then only contains functions if the ElementType value is char (second definition of WorkingSimpleVector_Base). Otherwise, it has no functions at all (first definition of WorkingSimpleVector_Base). The ThisClass parameter is what makes this a "CRTP" (Curiously Recurring Template Pattern). It allows the template to access the child class's fields through the use of the me() function. Remember that the template is no different from any other class so it won't have access to private members (unless the child class declares it as a friend).


Modified version of Yakk's answer explained

This first thing he/she declares is a helper template that does this whole conditional declaration for us:

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;

Variadic templates are scary, and I think they can be removed in this case. Let's simplify this to:

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;

conditional_apply_t's type type is an empty struct if the condition test is not true (see first definition of conditional_apply_t). If it is true, then the type type is the value of X. The definition of conditional_apply just removes the need for us to write ::type at the end of conditional_apply_t<...> every time we use this construct.

Next, we define a template that implements the behaviour we want

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;
  }
};

In this case, the D parameter is what gives us the "CRTP" (Curiously Recurring Template Pattern). See the above "Simple Answer" for more details on why this is important.

Next, we declare a type that only has this operator== function if a condition is satisfied:

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

So, the equal_string_helper<D,ET> type is:

  • An empty struct when ET != char
  • equal_string_helper_t<D> when ET == char

Finally, after all this, we can create the class we wanted with the following:

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

Which works as required.

解决方案

SFINAE works in a template function. In the context of template type substitution, substitution failure in the immediate context of the substitution is not an error, and instead counts as substitution failure.

Note, however, that there must be a valid substitution or your program is ill formed, no diagnostic required. I presume this condition exists in order that further "more intrusive" or complete checks can be added to the language in the future that check the validity of a template function. So long as said checks are actually checking that the template could be instantiated with some type, it becomes a valid check, but it could break code that expects that a template with no valid substitutions is valid, if that makes sense. This could make your original solution an ill formed program if there is no template type you could pass to the operator== function that would let the program compile.

In the second case, there is no substitution context, so SFINAE does not apply. There is no substitution to fail.

Last I looked at the incoming Concepts proposal, you could add requires clauses to methods in a template object that depend on the template parameters of the object, and on failure the method would not be considered for overload resolution. This is in effect what you want.

There is no standards-compliant way to do this under the current standard. The first attempt is one that may people commonly do, and it does compile, but it is technically in violation of the standard (but no diagnostic of the failure is required).

The standards compliant ways I have figured out to do what you want:

Changing one of the parameters of the method to be a reference to a never-completed type if your condition fails. The body of the method is never instantiate if not called, and this technique prevents it from being called.

Using a CRTP base class helper that uses SFINAE to include/exclude the method depending on an arbitrary condition.

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;
  }
};

where we do this:

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


We can refactor the conditional machinery out of the CRTP implementation if we choose:

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;

Then we split out the CRTP implementation without the conditional code:

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;
  }
};

then hook them up:

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

and we use it:

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

which looks identical at point of use. But the machinery behind was refactored, so, bonus?

这篇关于C ++模板:有条件启用的成员函数的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆