Question
How to Check Whether a Templated Class Has a Member Function in C++
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
Thas atoString()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()orstd::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.
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 intovoid.- If the expression is invalid, the specialization is ignored.
if constexprchooses 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)
- The compiler instantiates
optionalToString<User>. - It evaluates
has_toString<User>::value. - To do that, it checks whether
decltype(std::declval<User>().toString())is valid. Userdoes havetoString(), so the specializedhas_toStringis used.has_toString<User>::valuebecomestrue.if constexpr (true)keeps only this branch:
return obj->toString();
- The fallback branch is discarded.
Case 2: optionalToString<Point>(&point)
- The compiler instantiates
optionalToString<Point>. - It evaluates
has_toString<Point>::value. - It tries
decltype(std::declval<Point>().toString()). Pointdoes not havetoString(), so that substitution fails.- Because of SFINAE, this is not treated as a hard error.
- The compiler falls back to the primary template:
struct has_toString : std::false_type {};
has_toString<Point>::valuebecomesfalse.if constexpr (false)discards theobj->toString()branch.- 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 iterationsize()for container lengthreserve()for optimizationserialize()for custom persistenceswap()for efficient exchange
API wrappers
A wrapper around user-defined objects may support optional hooks such as:
validate()before saving datatoJson()before sending an API responsecleanup()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
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
Thas notoString(),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
| Approach | C++ Version | Best Use | Pros | Cons |
|---|---|---|---|---|
| SFINAE with overloads | C++11+ | Older codebases | Works without modern features | Harder to read |
Trait + std::void_t | C++17 commonly, possible earlier with custom void_t | Reusable detection | Clear and modular | Needs some template boilerplate |
if constexpr + trait | C++17+ | Branching inside one function | Very readable | Requires C++17 |
Concepts / requires | C++20+ | Clean constraints | Most expressive and readable | Requires C++20 |
if constexpr vs normal if
| Feature | if | if constexpr |
|---|---|---|
| Decides at runtime | Yes | No |
| Can discard invalid template branch | No | Yes |
| Good for type-based branching | No | Yes |
Detection of existence vs interface inheritance
| Technique | What it checks | Flexibility |
|---|---|---|
| Inheritance from base class | Explicit relationship | Lower |
| Detection idiom | Actual supported expression | Higher |
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";
}
}
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 constructingT. - Use
std::void_tin detection traits. - Use
if constexprfor compile-time branching. - Check the exact expression you need.
- If needed, also check return type convertibility.
- Be careful about
constcorrectness.
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 argumentstoString()exists only onconstobjectstoString()returns non-string typetoString()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.
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
formatObjectfunction that returns the result oftoString()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.
Keep learning
Related questions
Advantages of Brace Initialization in C++
Learn why C++ brace initialization is often clearer and safer than other object initialization styles, with examples and common pitfalls.
Basic Rules and Idioms for Operator Overloading in C++
Learn the core rules, syntax, and common idioms for operator overloading in C++, including member vs non-member operators.
C++ Aggregates, Trivial Types, Trivially Copyable Types, and PODs Explained
Learn what aggregates, trivial types, trivially copyable types, and PODs mean in C++, how they differ, and why they matter.