10

The program test tag dispatching pattern, where the function process_data(tag, a, b) can accept 2 tags int_tag, float_tag. There are 3 case:

  • int_tag and a, b are int -> print a + b
  • float_tag and a,b are float -> print a * b
  • print unsupported for every other case

The correct implementation is inside comment block. I'm trying to implement it another way, but current code always output unsupported isntead of calling the correct int/float version of process_data.

Why is the current implementation wrong? Compiled with -std=c++20

#include <iostream>
#include <type_traits>
#include <vector>
#include <string>

// ------------------------------------------------------------
// Tag types: int_tag and float_tag
// ------------------------------------------------------------
struct int_tag {};
struct float_tag {};

// ------------------------------------------------------------
// 2) Primary template: process_data (generic version)
// ------------------------------------------------------------
// template <typename Tag, typename T, typename U>
// void process_data(Tag tag, const T& a, const U& b) {
//     std::cout << "<unsupported type>" << "\n";
// }

template <typename Tag, typename A, typename B, typename Enable = void>
void process_data(Tag tag, const A& a, const B& b) {
    std::cout << "<unsupported type>" << "\n";
}

// ------------------------------------------------------------
// 3) Tag-dispatched function for int_tag (sum two integral numbers)
// ------------------------------------------------------------
// template <typename T, typename U>
// std::enable_if_t<std::is_integral_v<T> && std::is_integral_v<U>, void>
// process_data(int_tag, const T& a, const U& b) {
//     std::cout << "Result: " << (a + b) << "\n";  // Sum for integral types
// }

template <typename A, typename B, std::enable_if_t<std::is_integral_v<A> && std::is_integral_v<B>>>
void process_data(int_tag, const A& a, const B& b) {
    std::cout << "Result: " << (a + b) << "\n";
}

// ------------------------------------------------------------
// 4) Tag-dispatched function for float_tag (multiply two floating-point numbers)
// ------------------------------------------------------------
// template <typename T, typename U>
// std::enable_if_t<std::is_floating_point_v<T> && std::is_floating_point_v<U>, void>
// process_data(float_tag, const T& a, const U& b) {
//     std::cout << "Result: " << (a * b) << "\n";  // Product for floating-point types
// }

template <typename A, typename B, std::enable_if_t<std::is_floating_point_v<A> && std::is_floating_point_v<B>>>
void process_data(float_tag, const A& a, const B& b) {
    std::cout << "Result: " << (a * b) << "\n";
}

// ------------------------------------------------------------
// TESTS (do NOT change)
// ------------------------------------------------------------

int main() {
    bool all_ok = true;

    // Test: process_data for int_tag
    std::cout << "[process_data for int_tag]\n";
    std::cout << "int: ";
    process_data(int_tag{}, 10, 20);  // Should print: Result: 30
    std::cout << "float: ";
    process_data(float_tag{}, 3.14f, 2.0f);  // Should print: Result: 6.28
    std::cout << "string: ";
    process_data(int_tag{}, "Hello", "World");  // Should print: <unsupported type>

    return 0;
}
2
  • 1
    Why not use concepts? godbolt.org/z/vGcrEjofx Commented Nov 5 at 8:51
  • If A and B are both integral then std::enable_if_t<std::is_integral_v<A> && std::is_integral_v<B>> yields a non-type template parameter with incomplete type void making template impossible to instantiate. That's not how SFINAE supposed to work. see en.cppreference.com/w/cpp/language/sfinae.html Commented Nov 5 at 8:52

4 Answers 4

14

Issue is with your enable_if_t usage. With false condition SFINAE rejects it as intended, but with true condition it becomes template <typename A, typename B, void> which is also rejected by SFINAE.

It should be something like:

template <typename A,
          typename B,
          std::enable_if_t<std::is_integral_v<A> && std::is_integral_v<B>>* = nullptr>
//                                                                        ^^^^^^^^^^^
//                                                         add valid type and default
// or
//        std::enable_if_t<std::is_integral_v<A> && std::is_integral_v<B>, bool> = true>
void process_data(int_tag, const A& a, const B& b) {
    std::cout << "Result: " << (a + b) << "\n";
}

Demo

Your confusion is from

// 2) Primary template: process_data (generic version)

template <typename Tag, typename A, typename B, typename Enable = void>
void process_data(Tag, const A&, const B&);

There are no partial specializations for functions.
You just add overloads.
The typename Enable = void is so superfluous.

With C++20, you might simplify to:

template <std::integral A, std::integral B>
void process_data(int_tag, const A& a, const B& b) {
    std::cout << "Result: " << (a + b) << "\n";
}
template <std::floating_point A, std::floating_point B>
void process_data(float_tag, const A& a, const B& b) {
    std::cout << "Result: " << (a * b) << "\n";
}

Demo

Sign up to request clarification or add additional context in comments.

4 Comments

Ah, I understand it now. My enable_if_t is returning a specific type without default value. So when code in the main function call process_data without passing a value of that type into the template, it can't compile, so it chooses the default version instead. Thank you
@HuyLe, to be more precise, when code in the main function call process_data without passing a value of that type into the template, it can compile but the function you wanted to be called won't be considered by the compiler and thus will go back to the default version
std::enable_if_t<std::is_integral_v<A> && std::is_integral_v<B>>* = nullptr should read <std::is_integral_v<A> && std::is_integral_v<B>, bool> = true. Identical semantics, but the = true reads better (it is a lie, but a true lie!)
The constrained signature can be void process_data(float_tag, const std::floating_point auto &a, const std::floating_point auto & b) since the template parameters A and B don't need to be named.
5

Remark: the following doesn't answer your specific point (see the accepted answer for that)

Note however that instead of having some kind of runtime error (i.e. with a call to the "unsupported types" version), you could make sure at compile time that process_data can't be used with wrong arguments, and it is always better to detect errors at compile time rather than at runtime when possible.

You can do that with static_assert, e.g.

template <typename A, typename B>
void process_data(int_tag, const A& a, const B& b) {
    static_assert (std::is_integral_v<A> && std::is_integral_v<B>);
    std::cout << "Result: " << (a + b) << "\n";
}
template <typename A, typename B>
void process_data(float_tag, const A& a, const B& b) {
    static_assert (std::is_floating_point_v<A> && std::is_floating_point_v<B>);
    std::cout << "Result: " << (a * b) << "\n";
}

You don't need anymore to define a default process_data with a default "unsupported types" implementation

You can use it as follows:

int main() {
    std::cout << "int   : ";  process_data(int_tag{}, 10, 20);  // Should print: Result: 30
    std::cout << "float : ";  process_data(float_tag{}, 3.14f, 2.0f);  // Should print: Result: 6.28
    // would not compile:    std::cout << "string: ";  process_data(int_tag{}, "Hello", "World");  // Should print: <unsupported type>
    return 0;
}

Demo

4 Comments

those static asserts do not participate in overload resolution, so this is bad solution. I do not se how your answer helps.
@MarekR, fair enough. Kind of funny, I just added before your comment some disclaimer telling it does indeed not answer the specific point, but pinpoints the fact that SFINAE is not a good choice there when one can enforce a compile time error instead of a runtime one
template <typename Tag, typename A, typename B> void process_data(Tag tag, const A& a, const B& b) = delete; would produce the compile time error, in OP's code, if needed.
@Jarod42, good point. I guess the two options are acceptable: the "delete" one might be more configurable (easier to switch between a compile time or run time error) and the "static_assert" one emphasizes that there are only two valid overloads.
3

Not a direct answer, but might be useful:

Since you mentioned you use C++20 (-std=c++20), you could use Constraints and concepts to make the code more readable.

Below is an example using requires clauses:

#include <iostream>
#include <type_traits>

struct int_tag {};
struct float_tag {};

template <typename Tag, typename A, typename B>
void process_data(Tag, const A&, const B&) {
    std::cout << "<unsupported type>" << "\n";
}

template <typename A, typename B>
    requires std::integral<A> && std::integral<B>
void process_data(int_tag, const A& a, const B& b) {
    std::cout << "Result: " << (a + b) << "\n";
}

template <typename A, typename B>
    requires std::floating_point<A> && std::floating_point<B>
void process_data(float_tag, const A& a, const B& b) {
    std::cout << "Result: " << (a * b) << "\n";
}

int main() {
    std::cout << "[process_data for int_tag]\n";
    std::cout << "int: ";
    process_data(int_tag{}, 10, 20);  // Should print: Result: 30
    std::cout << "float: ";
    process_data(float_tag{}, 3.14f, 2.0f);  // Should print: Result: 6.28
    std::cout << "string: ";
    process_data(int_tag{}, "Hello", "World");  // Should print: <unsupported type>
}

Output:

[process_data for int_tag]
int: Result: 30
float: Result: 6.28
string: <unsupported type>

Live demo

4 Comments

std::same_as<Tag, int_tag> seems really verbose, just to use subsumption.
I removed some verbosity. I admit your code without the requires is the most elegant. But I'll keep my answer showing the usage of requires.
Are enable_if and tag dispatching obsolete in modern C++?
There might be edge cases for which they are required - to be honest I am not sure. But for the most part - yes: concepts and constraints pretty much made them obsolete for most cases.
2

Your current version is failing to work because your enable_if_t clauses are written wrong. It is wrong because you should stop using enable_if_t clauses in C++20 code.

A basic tag dispatching solution should look like this:

namespace processing { // always in a namespace
  // default implementation: (I usually =delete this)
  template <class Tag, class A, class B>
  void process_data(Tag, A const&, B const&) {
    std::cout << "<unsupported type>" << "\n";
  }

  // create both type and value tags:
  struct int_tag_t {};
  constexpr int_tag_t int_tag = {};
  struct float_tag_t {};
  constexpr float_tag_t float_tag = {};

  // tag overloads using concepts:
  void process_data(int_tag_t, std::integral auto const& a, std::integral auto const& b) {
    std::cout << "Result: " << (a + b) << "\n";
  }
  void process_data(float_tag_t, std::floating_point auto const& a, std::floating_point auto const& b) {
    std::cout << "Result: " << (a * b) << "\n";
  }
}
using processing::process_data; // if needed

remember, you can't partially specialize template functions; only overload them.

This has the problem that it effectively reserves process_data name in every namespace. To get around this, we can use a fancier customization point system using tag_invoke:

 namespace processing {
   struct process_data_cpo;
   template<class Tag>
   void tag_invoke( std::tag_t<process_data_cpo>, Tag, auto const&... );

   struct process_data_cpo {
     template<class Tag, class A, class B>
     constexpr void operator()(Tag tag, A const& a, B const& b)const{
       return tag_invoke( std::tag_t<process_data_cpo>{}, tag, a, b );
     }
   };
   constexpr process_data_cpo process_data;

   template<class Tag>
   void tag_invoke( process_data_cpo, Tag, auto&&... ) {
     std::cout << "<unsupported type>" << "\n";
   }
   
  // create both type and value tags:
  struct int_tag_t {};
  constexpr int_tag_t int_tag = {};
  struct float_tag_t {};
  constexpr float_tag_t float_tag = {};

  // tag overloads using concepts:
  void tag_invoke(std::tag_t<process_data_cpo>, int_tag_t, std::integral auto const& a, std::integral auto const& b) {
    std::cout << "Result: " << (a + b) << "\n";
  }
  void tag_invoke(std::tag_t<process_data_cpo>, float_tag_t, std::floating_point auto const& a, std::floating_point auto const& b) {
    std::cout << "Result: " << (a * b) << "\n";
  }
 }

now we use tag_invoke for the ADL portion, and we no longer have to reserve process_data in every namespace to avoid ADL quirks. To add customizations, you just override tag_invoke( std::tag_t<data_processing::process_data_cpo>, ... ) in either a namespace of your tag, a namespace of your types, or in data_processing namespace, and data_processing::process_data( tag, a, b ) will find it.

2 Comments

Do you mean that there's zero reason to use enable_if_t instead of requires/concept in C++20 and newer? And SFINAE using enable_if should always be replaced with requires/concept? Thanks!
It is possible that there may be a feature of SFINAE based enable_if that can't be done without it, but I can't think if one off-hand. Maybe some macro generated lambda stuff? The error messages and overload rules of requires are so much better, and the syntax is in 99% of cases more expressive.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.