C ++ Templates: Conditionally Activated Member Function

I am creating a very small C ++ project and I would like to create a simple vector class for my own needs. The std::vector template class will not work. When a vector class consists of char (i.e. vector<char> ), I would like it to be comparable to std::string . After a bit of fuss, I wrote code that 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 suppose I understand the cause of the error: the compiler creates a class definition for NotWorkingSimpleVector<int> , and then the type of my function operator== returned:

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

which then becomes:

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

which leads to an error: there is no type element in std::enable_if<false, bool> , which is really the integer point of the enable_if pattern.

I have two questions.

  1. Why doesn't SFINAE just disable the definition of operator== for NotWorkingSimpleVector<int> , how do I want it? Is there any compatibility reason for this? Are there any other use cases that I miss; Is there a reasonable counterargument for this behavior?
  2. Why WorkingSimpleVector first class ( WorkingSimpleVector ) work? It seems to me that the compiler “reserves the right to judge”: since the “ET” parameter is not yet defined, it stops trying to determine if operator== exists. Do we rely on “lack of understanding” by compilers to allow this type of conditionally included function (even if this “lack of understanding” is acceptable in the C ++ specification)?
+8
source share
3 answers

SFINAE works as a template function. In the context of pattern type substitution, substitution in the immediate context of substitution is not an error and instead is considered to be a replacement failure.

Please note, however, that there must be a valid replacement or if your program is poorly formed, no diagnostics are required. I suppose that this condition exists so that in the future more "intrusive" or full checks can be added to the language in the future, which check the correctness of the template function. As long as these checks actually verify that a template can be created by some type, it becomes a valid check, but it can break code that expects a template without valid substitutions to be valid, if that makes sense. This can make your original solution a poorly formed program if there is no type of template that you could pass to the operator== function that would allow compilation of the program.

In the second case, there is no substitution context, therefore SFINAE is not applied. There is no replacement for failure.

Recently, I looked at the incoming Concepts proposal, you can add requirements to the methods in the template object, which depend on the template parameters of the object, and if the method fails, it will not be taken into account when overload resolution is enabled. This is really what you want.

There is no standard way to do this in accordance with the current standard. The first attempt is an attempt that people usually make, and it compiles, but it technically violates the standard (but failure diagnosis is not required).

The standards-compliant ways I decided to do what you want:

Change one of the method parameters as a reference to a never completed type if your condition fails. The body of a method is never created unless called, and this method does not allow it to be called.

Using a helper class of the CRTP base class, which 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 do we do it:

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

We can reorganize the conditional mechanisms from the implementation of CRTP 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 the CRTP implementation without 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 plug them in:

 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 the same at the point of use. But the mechanism behind was reorganized, so, a bonus?

+3
source

The operator== templates basically make it irreproducible. You must explicitly do:

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

The simplest solution would be to simply add a non-member function:

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

To enable SFINAE and save it as a member function, you have to redirect your type to something else:

 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 } 
+2
source

Simple answer

This answer draws parallels with the answer to Yakk, but not so useful (Yakk supports arbitrary if-expressions). However, this is a little 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 using template specialization for the "if statements" we want. We base the class on WorkingSimpleVector_Base , which then contains functions only if the ElementType value is char (the second definition of WorkingSimpleVector_Base ). Otherwise, it has no functions at all (the first definition is WorkingSimpleVector_Base ). The ThisClass parameter makes it "CRTP" (a curiously repeating pattern) . It allows the template to access the fields of the child class using the me() function. Remember that the template is no different from any other class, so it will not have access to private members (unless the child class declares it as a friend ).


A modified version of Yakk's answer explained

This is the first thing he / she announces, this is an auxiliary template that executes for us all this conditional declaration:

 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 that in this case they can be deleted. Let me 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 type type is an empty structure if the test condition is not true (see the first definition of conditional_apply_t ). If true, then type is the value of X The definition of conditional_apply just eliminates the need to write ::type at the end of conditional_apply_t<...> every time we use this construct.

Next, we define a pattern that implements the behavior that 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, parameter D gives us "CRTP" (a curiously repeating pattern) . See the “Simple Answer” above to find out why this is important.

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

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

So, type equal_string_helper<D,ET> :

  • Empty structure when ET != char
  • equal_string_helper_t<D> when ET == char

Finally, after all this, we can create the desired class using 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.

0
source

Source: https://habr.com/ru/post/977414/


All Articles