0

I want to overload a method, based only on template argument (which is a type), without passing any method arguments. For that, I use code like this (simplified, of course) in C++17:

template<typename T>
class C {
    public:
        auto f() const {
            return f((T*) nullptr);
        }
    private:
        int f(int *) const {
            return 42;
        }
        std::string f(std::string *) const {
            return "hello";
        }
        std::vector<char> f(std::vector<char> *) const {
            return {'h', 'i'};
        }
};

dump(C<int>().f());
dump(C<std::string>().f());
dump(C<std::vector<char>>().f());

, which outputs:

42

"hello"

std::vector{'h', 'i'}

I am not a professional C++ developer, and this way looks hack-ish to me. Is there a better way to overload method f without using pointers as helper arguments? Or is this a "normal" solution?

P.S.: I'm not bonded to any particular C++ version.

Oleksandr Boiko
  • 182
  • 3
  • 9
  • 3
    Are you just looking to make a free function template? – Nathan Pierson Nov 30 '21 at 21:18
  • 4
    What's tough to know is if you actually want templates or not. Is it just three types? Do they need to behave differently for every type? Templates are great when the behavior is generic enough for *most* types. If you have to specialize every type, templates are unnecessary bloat. – sweenish Nov 30 '21 at 21:20
  • 3
    This has all the signs of an [XY problem](https://en.wikipedia.org/wiki/XY_problem) – Ted Lyngmo Nov 30 '21 at 21:25
  • 1
    It's difficult to help you because it's not clear to use what you want to achieve. As others have said, you could achieve the specific goal you've described without even having the class. – einpoklum Nov 30 '21 at 21:30
  • The modern way to do this is to use `if constexpr (std::is_same_v) ...` – Osyotr Nov 30 '21 at 21:33
  • 1
    @Genjutsu That's the C++17 way. Now there's also a C++20 way. – aschepler Nov 30 '21 at 21:33
  • @aschepler concepts, huh? I don't see how they can make things clearer. – Osyotr Nov 30 '21 at 21:36
  • @Genjutsu: You can split your functions up into actual functions with concepts/constraints instead of clumping all the impls into the same method. – AndyG Nov 30 '21 at 21:39
  • Sorry for confusions, probably I oversimplified my original case, so that the question is not really clear. Anyway, I think current @aschepler's answer gives me what I've searched for – Oleksandr Boiko Nov 30 '21 at 22:03

1 Answers1

2

(The comments on the question raise some valid questions about why you would have this. But just focusing on the question as asked.)

It is a bit hacky, but sometimes having an API function call an implementation function for technical reasons is a reasonable approach. One gotcha you might get into with that pattern is that pointers can automatically convert to other pointers: particularly T* to const T* and Derived* to Base*.

A rewrite using if constexpr (C++17 or later):

template <typename T>
T C<T>::f() const {
    if constexpr (std::is_same_v<T, int>) {
        return 42;
    } else if constexpr (std::is_same_v<T, std::string>) {
        // Note I changed the return type to T. If it's auto,
        // this would need return std::string{"hello"} so you don't
        // just return a const char*.
        return "hello";
    } else if constexpr (std::is_same_v<T, std::vector<char>>) {
        return {'h', 'i'};
    } else {
        // Always false (but don't let the compiler "know" that):
        static_assert(!std::is_same_v<T,T>,
                      "Invalid type T for C<T>::f");
    }
}

No more worries about pointer conversions, and this gives you a nicer error message if T is an unhandled type.

A rewrite using constraints (C++20 or later):

template<typename T>
class C {
    public:
        int f() const requires std::is_same_v<T,int> {
            return 42;
        }
        std::string f() const requires std::is_same_v<T,std::string> {
            return "hello";
        }
        std::vector<char> f() const
            requires std::is_same_v<T, std::vector<char>>
        {
            return {'h', 'i'};
        }
};

Now if a bad type T is used, the class doesn't contain any member named f at all! That could be better for other templates which might try checking if x.f() is a valid expression. And if a bad type is used, the compiler error might list the overloads which were declared but discarded from the class.

Quentin
  • 62,093
  • 7
  • 131
  • 191
aschepler
  • 70,891
  • 9
  • 107
  • 161
  • `static_assert(false)` is ill-formed, NDR – Osyotr Nov 30 '21 at 21:49
  • @Genjutsu Yup, fixed. (But I wish they would specifically allow it in a `constexpr if` substatement which is never instantiated.) – aschepler Nov 30 '21 at 21:53
  • `static_assert(!std::is_same_v` is also ill-formed. The common workaround is to use a type-dependent expression (https://en.cppreference.com/w/cpp/language/if#Constexpr_If), but it is probably still ill-formed (https://stackoverflow.com/a/62469617/5802715) – Osyotr Nov 30 '21 at 22:03
  • @Genjutsu Yeah, I'm not aware of a solution which is technically correct and simple. This one works with the major compilers, at least. – aschepler Dec 01 '21 at 00:12
  • @aschepler: I've used a non-specialized base template I call `always_false`: `template struct always_false : std::false_type{};`. You can `static_assert(always_false::value)` . It works because it allows the possibility of a specialization that would derive from `true_type` (a specialization that will never come...) – AndyG Dec 01 '21 at 02:20