Showing posts with label cpp_operator. Show all posts
Showing posts with label cpp_operator. Show all posts

Nov 2, 2026

[C++] ternary conditional operator

Reference:
[kernnel][C][C++] ternary conditional operator trick in action
Lightning Talk: ?: - Jonathan Müller - C++Now 2026

#include <iostream>
#include <type_traits>

int main() {
    bool cond = true;

    // 1. cond ? -1 : 1
    // Both operands are int, so the resulting type is int.
    auto r1 = cond ? -1 : 1;
    static_assert(std::is_same_v<decltype(r1), int>);

    // 2. cond ? -1 : 87654321
    // Both operands fit in int (decimal literal without suffix is int), resulting in int.
    auto r2 = cond ? -1 : 87654321;
    static_assert(std::is_same_v<decltype(r2), int>);

    // 3. cond ? -1 : 0x87654321
    // On systems with 32-bit int, 0x87654321 exceeds INT_MAX (2,147,483,647).
    // Hexadecimal integer literals without suffixes take the first type in:
    // int -> unsigned int -> long int -> unsigned long int ... that can represent the value.
    // Thus, 0x87654321 is typed as `unsigned int`.
    // By the usual arithmetic conversions for the ternary operator, 
    // int (-1) is converted to unsigned int, yielding an unsigned result.
    auto r3 = cond ? -1 : 0x87654321;
    static_assert(std::is_same_v<decltype(r3), unsigned int>);

    std::cout << "r1: " << r1 << " (type: int)\n";
    std::cout << "r2: " << r2 << " (type: int)\n";
    std::cout << "r3: " << r3 << " (type: unsigned int, value: " << r3 << ")\n";

    return 0;
}

#include <iostream>
#include <cstdint>
#include <type_traits>

int main() {
    bool cond = true;

    // 1. cond ? 'a' : 1
    // 'a' has type char, and 1 has type int.
    // By the usual arithmetic conversions, char is promoted to int.
    auto r1 = cond ? 'a' : 1;
    static_assert(std::is_same_v<decltype(r1), int>);

    // 2. cond ? 'a' : short(11)
    // Types smaller than int (here char and short) undergo integral promotion
    // to int before determining the common type, resulting in int.
    auto r2 = cond ? 'a' : short(11);
    static_assert(std::is_same_v<decltype(r2), int>);

    // 3. cond ? std::uint8_t(1) : std::int8_t(-1)
    // Both uint8_t and int8_t promote to int because int can represent 
    // all values of both types (0..255 and -128..127).
    auto r3 = cond ? std::uint8_t(1) : std::int8_t(-1);
    static_assert(std::is_same_v<decltype(r3), int>);

    std::cout << "r1: " << r1 << " (type: int)\n";
    std::cout << "r2: " << r2 << " (type: int)\n";
    std::cout << "r3: " << r3 << " (type: int)\n";

    return 0;
}

#include <iostream>
#include <type_traits>

enum A { a1, a2 };
enum B { b1, b2 };

int main() {
    bool cond = true;

    // 1. cond ? a1 : a2
    // Both operands are of the exact same unscoped enum type `A`,
    // so the resulting common type remains `A`.
    auto r1 = cond ? a1 : a2;
    static_assert(std::is_same_v<decltype(r1), A>);

    // 2. cond ? a1 : 11
    // One operand is enum `A` and the other is `int`.
    // Unscoped enums undergo integral promotion to their underlying type / `int`.
    // The common type resolves to `int`.
    auto r2 = cond ? a1 : 11;
    static_assert(std::is_same_v<decltype(r2), int>);

    // 3. cond ? a1 : b1
    // The operands are of distinct unscoped enum types `A` and `B`.
    // Both undergo integral promotions to `int` via the usual arithmetic conversions,
    // resulting in `int`.
    auto r3 = cond ? a1 : b1;
    static_assert(std::is_same_v<decltype(r3), int>);

    std::cout << "r1: " << r1 << " (type: A)\n";
    std::cout << "r2: " << r2 << " (type: int)\n";
    std::cout << "r3: " << r3 << " (type: int)\n";

    return 0;
}

#include <iostream>
#include <type_traits>

enum A { a1, a2 };

// "We have `std::constant_wrapper` at home"[cite: 4]
template <auto V>
using constant = std::integral_constant<decltype(V), V>;

int main() {
    bool cond = true;

    // 1. cond ? constant<0> : constant<0>
    // Both operands are the exact same type: constant<0>.
    auto r1 = cond ? constant<0>{} : constant<0>{};
    static_assert(std::is_same_v<decltype(r1), constant<0>>);

    // 2. cond ? constant<0> : constant<1>
    // Distinct types: std::integral_constant provides `constexpr operator value_type()`.
    // Both convert implicitly to int, so overload resolution selects int.
    auto r2 = cond ? constant<0>{} : constant<1>{};
    static_assert(std::is_same_v<decltype(r2), int>);

    // 3. cond ? constant<a1> : constant<a1>
    // Both operands are the exact same type: constant<a1>.
    auto r3 = cond ? constant<a1>{} : constant<a1>{};
    static_assert(std::is_same_v<decltype(r3), constant<a1>>);

    // 4. cond ? constant<a1> : constant<a2>
    // Both operands provide conversion operators to `A` (`operator A()`).
    // However, built-in conditional operator candidates [over.built] only exist 
    // for pointers, pointer-to-members, scoped enumerations, and promoted arithmetic types.
    // Because `A` is an unscoped enumeration, it undergoes integral promotion,
    // making `int operator?:(bool, int, int)` the candidate chosen, resulting in `int`.
    auto r4 = cond ? constant<a1>{} : constant<a2>{};
    static_assert(std::is_same_v<decltype(r4), int>);

    std::cout << "r1 type: constant<0>\n";
    std::cout << "r2 type: int\n";
    std::cout << "r3 type: constant<a1>\n";
    std::cout << "r4 type: int\n";

    return 0;
}

#include <iostream>
#include <cstdint>
#include <type_traits>

template <auto V>
using constant = std::integral_constant<decltype(V), V>;

int main() {
    bool cond = true;

    // 1. cond ? constant<'a'> : constant<short(11)>
    // Both operands are distinct types. Their implicit conversion operators
    // yield `char` and `short`, which both undergo integral promotion to `int`.
    // The built-in candidate `int operator?:(bool, int, int)` is selected.
    auto r1 = cond ? constant<'a'>{} : constant<short(11)>{};
    static_assert(std::is_same_v<decltype(r1), int>);

    // 2. cond ? constant<std::uint8_t(1)> : constant<std::int8_t(-1)>
    // Operands implicitly convert to `std::uint8_t` and `std::int8_t`.
    // Both types promote to `int` via the usual arithmetic conversions,
    // resulting in `int`.
    auto r2 = cond ? constant<std::uint8_t(1)>{} : constant<std::int8_t(-1)>{};
    static_assert(std::is_same_v<decltype(r2), int>);

    // 3. cond ? constant<std::uint8_t(1)> : constant<std::uint8_t(2)>
    // Even though both convert to the same underlying type (std::uint8_t),
    // they are distinct class template instantiations. Overload resolution for 
    // the conditional operator considers built-in candidate operators [over.built].
    // Built-in candidates exist only for promoted arithmetic types (at least `int`),
    // not for small types like `std::uint8_t`, so the common result type promotes to `int`.
    auto r3 = cond ? constant<std::uint8_t(1)>{} : constant<std::uint8_t(2)>{};
    static_assert(std::is_same_v<decltype(r3), int>);

    std::cout << "r1: " << r1 << " (type: int)\n";
    std::cout << "r2: " << r2 << " (type: int)\n";
    std::cout << "r3: " << r3 << " (type: int)\n";

    return 0;
}

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

int main() {
    bool cond = true;
    int i = 0;
    const int ci = 0;

    // 1. cond ? i : ci -> const int&
    // Both operands are lvalues differing only in cv-qualification.
    // The result is an lvalue of the cv-combined type (const int), yielding `const int&` under decltype.
    decltype(auto) r1 = cond ? i : ci; //[cite: 6]
    static_assert(std::is_same_v<decltype(cond ? i : ci), const int&>); //[cite: 6]

    // 2. cond ? std::move(i) : std::move(ci) -> const int&&
    // Both operands are xvalues (rvalue references) differing only in cv-qualification.
    // The result is an xvalue of the cv-combined type (const int), yielding `const int&&` under decltype.
    decltype(auto) r2 = cond ? std::move(i) : std::move(ci); //[cite: 6]
    static_assert(std::is_same_v<decltype(cond ? std::move(i) : std::move(ci)), const int&&>); //[cite: 6]

    // 3. cond ? i : std::move(ci) -> int
    // One operand is an lvalue (`int&`) and the other is an xvalue (`const int&&`).
    // Because value categories differ and cannot bind to a common reference category directly,
    // lvalue-to-rvalue conversion is applied to both, resulting in a prvalue of type `int`.
    decltype(auto) r3 = cond ? i : std::move(ci); //[cite: 6]
    static_assert(std::is_same_v<decltype(cond ? i : std::move(ci)), int>); //[cite: 6]

    std::cout << "r1: " << r1 << " (type: const int&)\n";
    std::cout << "r2: " << r2 << " (type: const int&&)\n";
    std::cout << "r3: " << r3 << " (type: int)\n";

    return 0;
}

Jun 11, 2022

[C++][C++20] consteval / constexpr





Within compile-time function, there is a difference between static typing of the code and dynamic computing of values.
consteval void process() {
    constexpr std::array a1{0, 8, 15};
    constexpr auto n1 = std::ranges::count(a1, 0); // OK
    std::array<int, n1> a1b; // OK

    std::array a2{0, 8, 15};
    // constexpr auto n2 = std::ranges::count(a2, 0); // ERROR

    std::array a3{0, 8, 15};
    auto n3 = std::ranges::count(a3, 0);
    // std::array<int, n3> a3b; // ERROR
}



std::is_constant_evaluated
Yields true when it called in a manifestly constant-evaluated expression or conversion.
That is roughly the case if we call it:
  • in a constant-expression, or
  • in a constant context (in 'if constexpr', a 'consteval function', or an 'constant initialization')
  • for an initializer of a variable usable at compile time
#include <type_traits>
#include <cstring>

constexpr int len(const char* s) {
	if (std::is_constant_evaluated()) {
    	// compile-time friendly code
		int idx = 0;
		while (s[idx] != '\0') { 			
			++idx;
		}
		return idx;
	} else {
		return std::strlen(s); // function called at runtime
	}
}
constexpr bool isConstEval() {
    return std::is_constant_evaluated();
}

bool g1 = isConstEval(); // true
const bool g2 = isConstEval(); // true
static bool g3 = isConstEval(); // true
static int g4 = g1 + isConstEval(); // false

int main() {
    bool l1 = isConstEval(); // false
    const bool l2 = isConstEval(); // true
    static bool l3 = isConstEval(); // true
    int l4 = g1 + isConstEval(); // false
    const int l5 = g1 + isConstEval(); // false
    static int l6 = g1 + isConstEval(); // false
    int l7 = isConstEval() + isConstEval(); // false
    const auto l8 = isConstEval() + 42 + isConstEval(); // true
}
bool runtimeFunc() {
	return std::is_constant_evaluated(); // always false
}
constexpr bool constexprFunc() {
	return std::is_constant_evaluated(); // may be false or true
}
consteval bool constevalFunc() {
	return std::is_constant_evaluated(); // always true
}
void foo() {
    bool b1 = runtimeFunc(); // F
    bool b2 = constexprFunc(); // F
    bool b3 = constevalFunc(); // T

    static bool sb1 = runtimeFunc(); // F
    static bool sb2 = constexprFunc(); // T
    static bool ab3 = constevalFunc(); // T

    const bool cb1 = runtimeFunc(); // F
    const bool cb2 = constexprFunc(); // T
    const bool cb3 = constevalFunc(); // T

    int y = 42;
    static bool sb4 = y + runtimeFunc(); // F
    static bool sb5 = y + constexprFunc(); // F
    static bool ab6 = y + constevalFunc(); // T

    const bool cb4 = y + runtimeFunc(); // F
    const bool cb5 = y + constexprFunc(); // F
    const bool cb6 = y + constevalFunc(); // T
}

When it makes no sense to use std::is_constant_evaluated()

As condition in a compile-time if, because that always yields true:
if constexpr (std::is_constant_evaluated()) {
        // always true
    }

Inside a pure runtime function, because that usually yields false.
The only exception is if it is used in a local constant evaluation:
void foo() {
    if (std::is_constant_evaluated()) {
        // always false
    }
    const bool b = std::is_constant_evaluated(); // true
}

Inside a consteval function, because that always yields true:
consteval void foo() {
    if (std::is_constant_evaluated()) { // always true
    }
}

Therefore, using std::is_constant_evaluated() usually only makes sense in constexpr functions.

std::is_constant_evaluated() and Operator ?: (conditional (ternary) operator)

int sz = 10;
constexpr bool sz1 = std::is_constant_evaluated() ? 20 : sz; // true, so 20
constexpr bool sz2 = std::is_constant_evaluated() ? sz : 20; // false, so ERROR

Reasoning as follows:
  • The initialization of sz1 and sz2 is either static initialization or dynamic initialization.
  • For static initialization, the initializer must be constant. So, the compiler attempts to evaluate the initializer with std::is_constant_evaluated() treated as a constant of value true.
  • Therefore, the previous result is discarded and the initializer is evaluated with std::is_constant_evaluated() producing false instead. So, the expression to initialize sz2 is also 20.
  • However, sz2 is not necessarily a constant, because std::is_constant_evaluated() is not necessarily a constant-expression during this evaluation. So, the initialization of sz2 with this 20 does not compile.

Feb 24, 2022

[kernnel][C][C++] ternary conditional operator trick in action

Reference:
Return type of '?:' (ternary conditional operator): https://stackoverflow.com/questions/8535226/return-type-of-ternary-conditional-operator

__is_constexpr() macro is dark magic: https://lore.kernel.org/linux-hardening/20220131204357.1133674-1-keescook@chromium.org/?fbclid=IwAR0Rgg_tGDk0qiEyuDsuZwERITSdstxmU2-bOtadb7iOOCtw3tgOPKEQ5hE

Using ternary conditional operator to get the type we want for a template type is often used in C++.

Here the same idea applies in C macro:

#define __is_constexpr(x) \
	(sizeof(int) == sizeof(*(8 ? ((void *)((long)(x) * 0l)) : (int *)8)))
Details:
 - sizeof() is an integer constant expression, and does not evaluate the
   value of its operand; it only examines the type of its operand.
 - The results of comparing two integer constant expressions is also
   an integer constant expression.
 - The use of literal "8" is to avoid warnings about unaligned pointers;
   these could otherwise just be "1"s.
 - (long)(x) is used to avoid warnings about 64-bit types on 32-bit
   architectures.
 - The C standard defines an "integer constant expression" as different
   from a "null pointer constant" (an integer constant 0 pointer).
 - The conditional operator ("... ? ... : ...") returns the type of the
   operand that isn't a null pointer constant. This behavior is the
   central mechanism of the macro.
 - If (x) is an integer constant expression, then the "* 0l" resolves it
   into a null pointer constant, which forces the conditional operator
   to return the type of the last operand: "(int *)".
 - If (x) is not an integer constant expression, then the type of the
   conditional operator is from the first operand: "(void *)".
 - sizeof(int) == 4 and sizeof(void) == 1.
 - The ultimate comparison to "sizeof(int)" chooses between either:
     sizeof(*((int *) (8)) == sizeof(int)   (x was a constant expression)
     sizeof(*((void *)(8)) == sizeof(void)  (x was not a constant expression)


For a conditional expression (?:) to be an lvalue, the second and third operands must be lvalues of the same type.
This is because the type and value category of a conditional expression is determined at compile time and must be appropriate whether or not the condition is true.
If one of the operands must be converted to a different type to match the other than the conditional expression cannot be an lvalue as the result of this conversion would not be an lvalue (but a r-value).

Thus:

OK:
int x = 1;
int y = 2;
(x > y ? x : y) = 100; // l-value on the left side of =

Not OK:
int x = 1;
long y = 2;
(x > y ? x : y) = 100; // type conversion to long as r-value type