Question

How to Check Whether a Templated Class Has a Member Function in C++

cpptemplatestemplate-meta-programmingsfinae

Question

In C++, is it possible to write a template that changes its behavior depending on whether a class defines a specific member function?

For example, suppose I want a function like this:

#include <string>

template<class T>
std::string optionalToString(T* obj)
{
    if (FUNCTION_EXISTS(T::toString))
        return obj->toString();
    else
        return "toString not defined";
}

The goal is:

  • If T has a toString() member function, call it.
  • Otherwise, return a fallback string such as "toString not defined".

How can the FUNCTION_EXISTS part be implemented in C++ templates?

Short Answer

By the end of this page, you will understand how C++ templates can detect whether a type provides a member function and then choose different behavior at compile time. You will learn the core idea behind SFINAE and the detection idiom, see modern and older C++ approaches, and build a practical optionalToString function that safely works with types that do or do not define toString().

Concept

In C++, templates are instantiated at compile time. That means the compiler generates code for a specific type only when that type is used. Because of this, C++ can make decisions based on what a type supports.

The core concept behind this question is compile-time detection of type capabilities. In generic programming, this is often called:

  • SFINAE: Substitution Failure Is Not An Error
  • detection idiom
  • in modern C++, sometimes constraints or if constexpr-based checks

The idea is simple:

  • Try to form an expression such as obj.toString() or std::declval<T>().toString().
  • If that expression is valid for type T, use one implementation.
  • If it is not valid, let the compiler discard that path and use another one.

This matters because generic code often needs to work with many types:

  • some types may support string conversion
  • some may support iteration
  • some may support comparison
  • some may provide custom APIs

Instead of forcing every type to inherit from a base class, C++ templates can detect behavior directly from the type itself. This is a major strength of C++ generic programming.

For this problem, the main question is not just "does the type have a member named toString?" but usually:

  • can toString() be called?
  • does it take no arguments?
  • does it return something convertible to std::string?

That is usually the most useful way to define the check.

Mental Model

Think of a template like a tool that tries a key in a lock.

  • If the key fits, the door opens and that version of the code is used.
  • If the key does not fit, the compiler quietly tries another door instead of crashing immediately.

In this example:

  • one door is the implementation for types that support toString()
  • the other door is the fallback implementation

So the compiler is effectively asking:

"Can I call toString() on this type?"

If yes, it uses that path. If no, it uses the fallback path.

This is a compile-time decision, not a runtime if statement.

Take Quiz

Syntax and Examples

A modern C++ approach uses std::void_t, decltype, and partial specialization to detect whether T has a usable toString() member.

#include <iostream>
#include <string>
#include <type_traits>
#include <utility>

// Primary template: assume no toString()
template <typename, typename = std::void_t<>>
struct has_toString : std::false_type {};

// Specialization: valid if T has a callable toString()
template <typename T>
struct has_toString<T, std::void_t<decltype(std::declval<T>().toString())>>
    : std::true_type {};

template <typename T>
std::string optionalToString(T* obj)
{
    if constexpr (has_toString<T>::value) {
        return obj->toString();
    } else {
        return "toString not defined";
    }
}

struct WithToString {
    std::string toString() {
        return "I have toString";
    }
};

struct WithoutToString {};

int main()
{
    WithToString a;
    WithoutToString b;

    std::cout << optionalToString(&a) << '\n';
    std::cout << optionalToString(&b) << '\n';
}

This prints:

I have toString
toString not defined

What this does

  • std::declval<T>().toString() is used only to test whether the expression is valid.
  • decltype(...) asks the compiler for the type of that expression.
  • std::void_t<...> turns a valid expression into void.
  • If the expression is invalid, the specialization is ignored.
  • if constexpr chooses the valid branch at compile time.

Checking the return type too

If you want to require that toString() returns something convertible to std::string, you can make the trait stricter:

#include <string>
#include <type_traits>
#include <utility>

template <typename, typename = std::void_t<>>
struct has_string_toString : std::false_type {};

template <typename T>
struct has_string_toString<
    T,
    std::void_t<decltype(std::declval<T>().toString())>
> : std::is_convertible<decltype(std::declval<T>().toString()), std::string> {};

Now the trait is only true if toString() exists and its result can be used as a std::string.

Step by Step Execution

Consider this small example:

#include <string>
#include <type_traits>
#include <utility>

template <typename, typename = std::void_t<>>
struct has_toString : std::false_type {};

template <typename T>
struct has_toString<T, std::void_t<decltype(std::declval<T>().toString())>>
    : std::true_type {};

template <typename T>
std::string optionalToString(T* obj)
{
    if constexpr (has_toString<T>::value) {
        return obj->toString();
    }
    return "toString not defined";
}

struct User {
    std::string toString() {
        return "User object";
    }
};

struct Point {};

Case 1: optionalToString<User>(&user)

  1. The compiler instantiates optionalToString<User>.
  2. It evaluates has_toString<User>::value.
  3. To do that, it checks whether decltype(std::declval<User>().toString()) is valid.
  4. User does have toString(), so the specialized has_toString is used.
  5. has_toString<User>::value becomes true.
  6. if constexpr (true) keeps only this branch:
return obj->toString();
  1. The fallback branch is discarded.

Case 2: optionalToString<Point>(&point)

  1. The compiler instantiates optionalToString<Point>.
  2. It evaluates has_toString<Point>::value.
  3. It tries decltype(std::declval<Point>().toString()).
  4. Point does not have toString(), so that substitution fails.
  5. Because of SFINAE, this is not treated as a hard error.
  6. The compiler falls back to the primary template:
struct has_toString : std::false_type {};
  1. has_toString<Point>::value becomes false.
  2. if constexpr (false) discards the obj->toString() branch.
  3. Only the fallback remains:
return "toString not defined";

The important detail is that the invalid branch is never compiled for Point. That is why this works safely.

Real World Use Cases

This pattern appears often in real C++ code, especially in generic libraries and reusable utilities.

Logging and debugging

A logging utility may prefer a type's custom toString() if available:

logger.write(optionalToString(&obj));

Serialization helpers

A serializer might check whether a type provides its own formatting or serialization method before using a default strategy.

Generic library code

Template-based libraries often detect capabilities such as:

  • begin() / end() for iteration
  • size() for container length
  • reserve() for optimization
  • serialize() for custom persistence
  • swap() for efficient exchange

API wrappers

A wrapper around user-defined objects may support optional hooks such as:

  • validate() before saving data
  • toJson() before sending an API response
  • cleanup() before destruction

Fallback behavior

Sometimes you want the nicest available behavior without forcing all types to implement the same interface. Detection lets you write:

  • custom path if supported
  • safe fallback otherwise
Take Quiz

Real Codebase Usage

In real codebases, developers rarely write a fake macro like FUNCTION_EXISTS(...). Instead, they usually use one of these patterns.

1. Traits for capability checks

A trait like has_toString<T> is reusable and keeps the check in one place.

if constexpr (has_toString<T>::value) {
    // custom path
} else {
    // fallback
}

This is clean and easy to test.

2. Guarded generic functions

Developers often use detection to enable or disable overloads.

template <typename T>
auto stringify(const T& value) -> decltype(value.toString(), std::string()) {
    return value.toString();
}

This style is common in older C++ code before if constexpr.

3. Validation of optional APIs

Libraries often check not just whether a function exists, but whether it has the right shape:

  • correct name
  • correct argument list
  • correct return type
  • correct const qualification

For example, toString() const is different from toString().

4. Early fallback patterns

Generic code often prefers a specialized operation when possible, otherwise a default implementation.

Examples:

  • use .reserve(n) if the container supports it
  • use .size() if available, otherwise count manually
  • use a member serialize() if present, otherwise serialize fields generically

5. Error handling and extension points

Frameworks use detection for optional extension hooks. For instance:

  • call beforeSave() if a model defines it
  • call onError(err) if a handler provides it
  • call reset() if a component supports state reset

This lets code remain flexible without forcing inheritance everywhere.

Common Mistakes

1. Trying to use a normal runtime if

A regular if does not prevent compilation of invalid code in templates.

Broken idea:

template <typename T>
std::string optionalToString(T* obj) {
    if (has_toString<T>::value)
        return obj->toString();
    else
        return "toString not defined";
}

Why this can fail in older styles:

  • both branches may still need to be valid during compilation
  • if T has no toString(), obj->toString() can still cause an error

Use if constexpr in C++17+.

2. Checking only for the name, not the call

A member function may exist but not match what you need.

Examples:

  • it may require parameters
  • it may be overloaded
  • it may return the wrong type
  • it may be private

Better check the exact expression you intend to use:

std::declval<T>().toString()

3. Forgetting const

This is very common.

Broken expectation:

struct A {
    std::string toString() const;
};

If your check uses std::declval<T>().toString(), it tests a non-const object. If your real code works with const T&, you may need:

decltype(std::declval<const T&>().toString())

4. Ignoring the return type

A class might have toString() but return something unexpected.

struct Weird {
    int toString() { return 42; }
};

If your function promises std::string, check convertibility.

5. Using macros for this

A preprocessor macro cannot reliably inspect C++ type members the way templates and SFINAE can.

Avoid trying to build this with #define FUNCTION_EXISTS(...).

6. Confusing member functions with free functions

This page checks for a member like:

obj.toString()

That is different from checking for a free function like:

toString(obj)

The detection code changes depending on which form you need.

Comparisons

ApproachC++ VersionBest UseProsCons
SFINAE with overloadsC++11+Older codebasesWorks without modern featuresHarder to read
Trait + std::void_tC++17 commonly, possible earlier with custom void_tReusable detectionClear and modularNeeds some template boilerplate
if constexpr + traitC++17+Branching inside one functionVery readableRequires C++17
Concepts / requiresC++20+Clean constraintsMost expressive and readableRequires C++20

if constexpr vs normal if

Featureifif constexpr
Decides at runtimeYesNo
Can discard invalid template branchNoYes
Good for type-based branchingNoYes

Detection of existence vs interface inheritance

TechniqueWhat it checksFlexibility
Inheritance from base classExplicit relationshipLower
Detection idiomActual supported expressionHigher

C++20 requires version

If you use C++20, the code becomes much simpler:

#include <string>

template <typename T>
std::string optionalToString(T* obj)
{
    if constexpr (requires(T t) { t.toString(); }) {
        return obj->toString();
    } else {
        return "toString not defined";
    }
}

If you also want the return type to be convertible to std::string:

#include <concepts>
#include <string>

template <typename T>
std::string optionalToString(T* obj)
{
    if constexpr (requires(T t) {
        { t.toString() } -> std::convertible_to<std::string>;
    }) {
        return obj->toString();
    } else {
        return "toString not defined";
    }
}
Take Quiz

Cheat Sheet

// Detect whether T has callable toString()
template <typename, typename = std::void_t<>>
struct has_toString : std::false_type {};

template <typename T>
struct has_toString<T, std::void_t<decltype(std::declval<T>().toString())>>
    : std::true_type {};
// Use it with if constexpr
template <typename T>
std::string optionalToString(T* obj)
{
    if constexpr (has_toString<T>::value) {
        return obj->toString();
    } else {
        return "toString not defined";
    }
}

Quick rules

  • Use decltype(expr) to test whether an expression is valid.
  • Use std::declval<T>() to form expressions without constructing T.
  • Use std::void_t in detection traits.
  • Use if constexpr for compile-time branching.
  • Check the exact expression you need.
  • If needed, also check return type convertibility.
  • Be careful about const correctness.

Useful variations

Check non-const call:

decltype(std::declval<T>().toString())

Check const call:

decltype(std::declval<const T&>().toString())

Check return type is convertible to std::string:

std::is_convertible<decltype(std::declval<T>().toString()), std::string>::value

C++20 shortcut

if constexpr (requires(T t) { t.toString(); })

Common edge cases

  • toString() exists but takes arguments
  • toString() exists only on const objects
  • toString() returns non-string type
  • toString() is private
  • overloaded toString() may need more precise checks

FAQ

How do I check if a class has a member function in C++?

Use template metaprogramming techniques such as SFINAE, std::void_t, or C++20 requires expressions to test whether an expression like obj.toString() is valid.

Can I use a macro to detect whether a member function exists?

Not reliably. The C++ preprocessor does not understand class members or template substitution rules. Use traits or requires instead.

What is SFINAE in simple terms?

SFINAE means that if a template substitution creates an invalid type or expression in certain contexts, the compiler ignores that candidate instead of treating it as a hard error.

Why doesn't a normal if work for this?

Because template code must still be valid during compilation. A normal if does not discard invalid code. if constexpr does.

How do I check for toString() const specifically?

Use a const-qualified expression in the trait, such as:

decltype(std::declval<const T&>().toString())

Can I also check the return type of the member function?

Yes. Combine the existence check with std::is_convertible or, in C++20, use a requires expression with std::convertible_to<std::string>.

Is this better than requiring a base class?

It depends. Detection is more flexible because it works with any type that supports the needed operation, even if it does not inherit from a shared base class.

What is the modern C++20 way to do this?

Use a requires expression:

if constexpr (requires(T t) { t.toString(); })

This is the clearest modern approach.

Take Quiz

Mini Project

Description

Build a small generic string formatter for C++ objects. The formatter should use an object's own toString() method when available and fall back to a default message when it is not. This demonstrates compile-time detection of member functions and safe branching in template code.

Goal

Create a reusable formatObject function that works with different types and automatically chooses the best available formatting behavior.

Requirements

  • Create a trait that detects whether a type has a callable toString() member function.
  • Write a generic formatObject function that returns the result of toString() when available.
  • Return a fallback string when toString() is not defined.
  • Test the function with at least one type that has toString() and one type that does not.
  • Keep the implementation valid C++17 or later.
Take Quiz

Keep learning

Related questions

View more