C++ Concepts: How to Identify Which Part of a Fold Expression Failed
C++20 concepts are incredibly powerful for constraining template parameters and providing expressive API interfaces. However, when you use fold expressions (such as (&& ...)) directly inside a concept definition, compiler diagnostics can sometimes leave you in the dark. Instead of pinpointing exactly which type in a parameter pack failed the constraint, compilers like GCC and Clang often output a generic error stating that the entire fold expression evaluated to false.
In this article, we will look at why this happens and how to refactor your concepts so that the compiler tells you exactly which type failed to satisfy your constraints.
The Problem: Opaque Fold Expression Diagnostics
Let's look at a simplified version of the problem. Suppose we have a concept that checks if a trait class has a static update function for a pack of types:
#include <type_traits>
#include <vector>
#include <variant>
template<template<class> class UpdateResultType, class Sink, class T>
using update_func_t = UpdateResultType<T> (*)(Sink&, T);
template<class UpdateTraits, template<class> class UpdateResultType, class Sink, class... Types>
concept update_traits = (requires(update_func_t<UpdateResultType, Sink, Types>& cb)
{
{ cb = &UpdateTraits::update };
}
&& ...);
If we try to use this concept but miss one of the overloads in our implementation, the compiler output is notoriously unhelpful:
the expression '(requires(UpdateResultType<Types> (*&cb)(Sink&, Types)) {{cb =&(UpdateTraits::update)};} && ...)' evaluated to 'false'
The compiler tells you that the fold expression failed, but it doesn't tell you which type in the Types... pack caused the failure. If your pack has dozens of types, tracking down the missing overload can become a tedious guessing game.
The Solution: Extract a Helper Concept
To get the compiler to pinpoint the exact failure, we need to break the fold expression apart. Instead of folding a raw requires block, we define a unary helper concept that checks a single type, and then fold over that helper concept.
Here is how you can refactor your code:
#include <type_traits>
#include <vector>
#include <variant>
template<template<class> class UpdateResultType, class Sink, class T>
using update_func_t = UpdateResultType<T> (*)(Sink&, T);
// 1. Define a helper concept for a single type
template<class UpdateTraits, template<class> class UpdateResultType, class Sink, class T>
concept single_update_trait = requires(update_func_t<UpdateResultType, Sink, T>& cb)
{
{ cb = &UpdateTraits::update };
};
// 2. Fold over the helper concept instead of a raw requires-expression
template<class UpdateTraits, template<class> class UpdateResultType, class Sink, class... Types>
concept update_traits = (single_update_trait<UpdateTraits, UpdateResultType, Sink, Types> && ...);
Why This Works
When the compiler evaluates (single_update_trait<UpdateTraits, UpdateResultType, Sink, Types> && ...), it treats each instantiation of single_update_trait as an independent constraint.
If the concept evaluation fails, the compiler will trace the failure down to the specific instantiation of the helper concept. The diagnostic output will look something like this:
<source>: In instantiation of 'concept update_traits<my_traits, std::add_pointer_t, std::vector<std::variant<int, double>>&, int, double>':
...
<source>: note: nested requirement 'single_update_trait<my_traits, std::add_pointer_t, std::vector<std::variant<int, double>>&, double>' was not satisfied
Notice how the compiler explicitly points out that single_update_trait failed for the type double. This instantly tells you that the overload for double is what's missing or mismatched in your trait structure.
Summary of Best Practices
- Avoid raw fold expressions on
requiresblocks: Writing(requires(...) { ... } && ...)prevents the compiler from giving granular diagnostic feedback. - Leverage Helper Concepts: Always extract the inner requirement into a standalone, single-parameter concept.
- Improved Readability: Splitting concepts not only improves compiler errors but also makes your code significantly easier to read, maintain, and reuse.