License : Creative Commons Attribution 4.0 International (CC BY-NC-SA 4.0)
Copyright : Hervé Frezza-Buet, CentraleSupelec
Last modified : April 19, 2024 10:22
Link to the source : sfinae.md

SFINAE

/*

  g++ -o test example-sfinae-001.cpp -std=c++20 -lpthread

*/

#include <iostream>

// Let us define a clear_if_clear_available<PROCESS, DUMMY_TYPE>
// template. The dummy type can be omitted, since its default value is
// defined to be void.
//
// clear_if_clear_available<PROCESS> cica;
//
// ... declares an instance of the following class, where operator()
// do nothing, and where the DUMMY_TYPE is void... and useless.
template<typename PROCESS, typename = void>
struct clear_if_clear_available {
  void operator()(const PROCESS& d) {}
};


// The previous can change if we specify the template. Indeed, if we define
//
// template<typename PROCESS>
// struct clear_if_clear_available<PROCESS, int> {...};
//
// ... the compiler will use this specification if we only declare
//
// clear_if_clear_available<PROCESS> cica;
//
// ... as previously. The trick is to define such a specification,
// allowing a syntax that overshadoes the default declaration (with
// its default second type variable). But the point is that instead of
// int, we will use the template compiling rules to generate a type
// from PROCESS. It if fails, only the general class defined previously
// is avaiable. If it succeeds, the second one overshadoes the
// previous class.
//
// std::declval<PROCESS>() is a "syntaxical" instance of the type
// PROCESS. So std::declval<PROCESS>().clear() is the syntactical
// expression invoking the method clear of the class PROCESS. Its type
// is decltype(std::declval<PROCESS>().clear()). The compiler can
// infer it only from its type checking mechanism. If clear is not
// defined for the class used as the PROCESS type, you get a syntax
// error... which is not a failure since the compiler only aborts the
// definition of this specification of clear_if_clear_available.
template<typename PROCESS>
struct clear_if_clear_available<PROCESS, decltype(std::declval<PROCESS>().clear())> {
  void operator()(const PROCESS& d) {d.clear();}
};



// Ok, now we have a template, that we will always call with a single
// type whereas it has two parameters. This leads to the first or the
// second version of the clear_if_clear_available, depending on the
// syntactical validity of the expression me.clear(), when me is an
// instance of PROCESS.
template<typename PROCESS> void execute_process(const PROCESS& process, unsigned int nb_times) {
  clear_if_clear_available<PROCESS> clear_if_allowed {}; // Clear is used in operator() only if it is syntaxically available.
  clear_if_allowed(process);                             // Calls clear, or do nothing according to the template instanciation.
  for(unsigned int i = 0; i < nb_times; ++i) process(i);
}


// This process do not need to be cleared beforehand.
void process1(unsigned int arg) {
  std::cout << "process1(" << arg << ")" << std::endl;
}

// This process do need to be cleared beforehand.
struct Process2 {

  mutable bool cleared = false;
  
  void operator()(unsigned int arg) const {
    if(cleared)
      std::cout << "Process2(" << arg << ")" << std::endl;
    else
      throw "Process2 is used whithout previous clear.";
  }

  void clear() const {
    cleared = true; // ok with const, it is mutable.
    std::cout << "Process2::clear()" << std::endl;
  }
};


int main(int argc, char* argv[]) {

  Process2 process2;

  std::cout << std::endl;
  execute_process(process1, 5);
  std::cout << std::endl;
  execute_process(process2, 5);
  std::cout << std::endl;
  
  return 0;
}
Hervé Frezza-Buet,