CodeNebula
C++20coreadvanced

Conditional explicit (explicit(bool)) (C++20 Addition)

C++20 · The Big Four · Compiler flag: -std=c++20

Explanation

The `explicit` specifier can now accept a boolean expression, enabling constructors and conversion operators to be conditionally explicit based on compile-time conditions. This is essential for wrapper types that should be implicitly convertible only when their underlying type supports implicit conversion.

Real-World Scenario

Build a generic `Optional<T>` wrapper that is implicitly constructible from `T` only when `T` itself is implicitly convertible from the source type, preventing accidental narrowing conversions in a type-safe API.

Quick Snippet

snippet.cpp
#include <type_traits>

template<typename T>
struct Wrapper {
    T value;

    template<typename U>
    explicit(!std::is_convertible_v<U, T>)
    Wrapper(U&& u) : value(std::forward<U>(u)) {}
};

// Wrapper<int> w = 3.14;   // OK: double is convertible to int
// Wrapper<int> w2 = "hi";  // Error: const char* not convertible to int

Full Example

conditional_explicit.cpp
// CodeNebula - C++20 Example: Conditional explicit (explicit(bool))
// Compile: g++ -std=c++20 -Wall -Wextra -o example conditional_explicit.cpp

// C++20 allows explicit(bool-expression) to conditionally make a
// constructor or conversion explicit. This is useful in wrapper types
// that should mirror the implicit/explicit behavior of the wrapped type.

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

// A wrapper that is implicitly convertible only when T is
// implicitly convertible from the argument type
template <typename T>
struct Wrapper {
    T value;

    // If U is implicitly convertible to T, this constructor is implicit.
    // Otherwise, it is explicit.
    template <typename U>
    explicit(!std::is_convertible_v<U, T>)
    Wrapper(U&& val) : value(std::forward<U>(val)) {}

    friend std::ostream& operator<<(std::ostream& os, const Wrapper& w) {
        os << w.value;
        return os;
    }
};

// A pair type where explicitness depends on BOTH element types
template <typename T1, typename T2>
struct MyPair {
    T1 first;
    T2 second;

    template <typename U1, typename U2>
    explicit(!std::is_convertible_v<U1, T1> || !std::is_convertible_v<U2, T2>)
    MyPair(U1&& a, U2&& b)
        : first(std::forward<U1>(a)), second(std::forward<U2>(b)) {}
};

// Demonstrate with a type that has an explicit-only constructor
struct StrictInt {
    int value;
    explicit StrictInt(int v) : value(v) {}  // no implicit conversion from int
};

// A numeric type with conditional explicit conversion operator
template <typename T>
struct Number {
    T val;

    Number(T v) : val(v) {}

    // Implicit conversion to U only if T is implicitly convertible to U
    template <typename U>
    explicit(!std::is_convertible_v<T, U>)
    operator U() const { return static_cast<U>(val); }
};

void takes_wrapper_int(Wrapper<int> w) {
    std::cout << "  Wrapper<int>: " << w << "\n";
}

void takes_wrapper_string(Wrapper<std::string> w) {
    std::cout << "  Wrapper<string>: " << w << "\n";
}

int main() {
    std::cout << "=== explicit(bool) — Conditional Explicit ===\n\n";

    // --- Wrapper examples ---
    // int -> int is implicit, so Wrapper<int> from int is implicit
    Wrapper<int> w1 = 42;             // OK: implicit
    std::cout << "Wrapper<int> from int: " << w1 << "\n";

    // const char* -> std::string is implicit
    Wrapper<std::string> w2 = "hello"; // OK: implicit
    std::cout << "Wrapper<string> from const char*: " << w2 << "\n";

    // double -> int is narrowing but convertible
    Wrapper<int> w3 = 3.14;           // OK: double is convertible to int
    std::cout << "Wrapper<int> from double: " << w3 << "\n\n";

    // --- Function call demonstrates implicit conversion ---
    std::cout << "Implicit conversions in function calls:\n";
    takes_wrapper_int(100);            // OK: int -> Wrapper<int> is implicit
    takes_wrapper_string("world");     // OK: const char* -> string is implicit
    std::cout << "\n";

    // --- MyPair examples ---
    // Both conversions are implicit -> MyPair is implicit
    MyPair<int, double> p1 = {1, 2.5};
    std::cout << "MyPair<int,double>: " << p1.first << ", " << p1.second << "\n";

    // --- Number conversion ---
    Number<int> n{42};
    double d = n;     // int -> double is implicit, so this works
    std::cout << "Number<int> to double: " << d << "\n";

    // int -> std::string is NOT convertible, so this would fail:
    // std::string s = n;  // ERROR: explicit conversion required

    std::cout << "\nKey insight: explicit(condition) lets generic wrappers\n"
              << "mirror the implicit/explicit behavior of their contents.\n";

    return 0;
}

// Expected output:
// === explicit(bool) — Conditional Explicit ===
//
// Wrapper<int> from int: 42
// Wrapper<string> from const char*: hello
// Wrapper<int> from double: 3
//
// Implicit conversions in function calls:
//   Wrapper<int>: 100
//   Wrapper<string>: world
//
// MyPair<int,double>: 1, 2.5
// Number<int> to double: 42
//
// Key insight: explicit(condition) lets generic wrappers
// mirror the implicit/explicit behavior of their contents.
Run command
g++ -std=c++20 -Wall -Wextra -o example conditional_explicit.cpp