How std::is_convertible Works in C++

In the C++ programming language, std::is_convertible<From, To> is a standard type trait defined in the <type_traits> header that determines at compile time whether an expression of type From can be implicitly converted to type To. This article covers the underlying mechanics of std::is_convertible, explaining how it evaluates conversion viability through copy-initialization semantics, handles access control and ambiguity, and applies special rules for types such as void, arrays, and abstract classes.

The Conceptual Model: Imaginary Function Returns

The C++ standard defines the behavior of std::is_convertible<From, To> using an imaginary function definition. Specifically, given the types From and To, the trait evaluates whether the following function template is well-formed:

To test() {
    return std::declval<From>();
}

If a hypothetical function defined this way compiles without errors, std::is_convertible<From, To>::value evaluates to true. If the statement produces a substitution failure or a semantic error, it evaluates to false.

Because the evaluation uses return std::declval<From>();, it tests whether an invented lvalue or rvalue expression of type From can initialize a return value of type To.

Copy-Initialization vs. Direct-Initialization

The core distinction of std::is_convertible is that it strictly tests implicit conversion via copy-initialization contexts. It is functionally equivalent to testing whether the declaration To obj = std::declval<From>(); is valid.

This leads to critical behavioral distinctions:

  • Implicit Constructors and Conversions: If To has an implicit constructor accepting From, or if From has a non-explicit conversion operator operator To(), the trait yields true.
  • Explicit Constructors and Conversions: If To declares a constructor taking From as explicit, or if From defines explicit operator To(), std::is_convertible<From, To> evaluates to false. In scenarios where direct-initialization (To obj(std::declval<From>());) is intended, std::is_constructible<To, From> must be used instead.

Access Control and Ambiguity Resolution

When evaluating conversion viability, std::is_convertible respects C++ access control and overload resolution rules:

  1. Access Control: If converting From to To requires access to a private or protected constructor, or involves private or protected base class inheritance, the conversion is considered ill-formed, and the trait evaluates to false.
  2. Ambiguity: If multiple conversion paths exist—such as ambiguous multiple inheritance conversions or competing conversion operators with equal viability—the conversion is ambiguous. This causes SFINAE (Substitution Failure Is Not An Error) mechanisms to fail, yielding false.

Handling Special Types

The standard specifies dedicated rules for edge-case types that cannot participate in normal function return statements:

  • Void Types: Regular function semantics forbid returning values of or to void in this context. However, std::is_convertible explicitly defines that std::is_convertible<cv void, cv void> is true. Converting any other type to void, or void to any non-void type, always results in false.
  • Arrays and Function Types: A function cannot return an array type or a function type by value. Consequently, if To is an array type (e.g., int[] or int[5]) or a function type (e.g., void()), the trait always evaluates to false, regardless of From.
  • Abstract Classes: Because an abstract class cannot be instantiated or returned by value, any attempt to evaluate std::is_convertible<From, AbstractClass> where To is not a reference type evaluates to false.
  • Incomplete Types: Both From and To must be complete types, complete arrays, or (possibly cv-qualified) void; passing an incomplete class type yields undefined behavior or a compile-time failure.

Implementation: Compiler Built-ins vs. SFINAE

In modern standard library implementations (such as libstdc++, libc++, and MSVC STL), std::is_convertible is rarely implemented purely through user-space SFINAE overloads like sizeof tricks. Instead, modern compilers provide internal compiler built-ins—such as __is_convertible_to(From, To) or __is_convertible(From, To).

These intrinsics allow the compiler to perform the viability check directly inside the frontend AST analyzer. This avoids heavy template instantiation overhead, reduces compiler memory consumption, and guarantees strict conformity with standard corner cases such as non-trivial destructors and access-checking contexts.