I am defining a way to know the position of the type in a type list, using recursive templates in C++17. I tried two ways : one using constexpr value and one using constexpr function. The second, using if statement, compiles, while the first, using ternary operator, does not compile.
#include <type_traits>
#include <iostream>
template<typename Searching, typename First, typename...Others>
constexpr std::size_t index_of = std::is_same_v<Searching,First> ? 0 : 1 + index_of<Searching,Others...>;
template<typename Searching, typename First, typename...Others>
constexpr std::size_t IndexOf() {
if constexpr(std::is_same_v<Searching,First>)
return 0;
else return 1 + IndexOf<Searching,Others...>();
};
int main() {
std::cout << index_of<int, int> << std::endl; //does not compile
std::cout << IndexOf<int, int>() << std::endl; //compile
//both should return 0
return 0;
}
My compiler, migw64, says :
wrong number of template arguments (1, should be at least 2)
constexpr std::size_t index_of = std::is_same_v<Searching,First> ? 0 : 1 + index_of<Searching,Others...>;
From what I understand, the ternary operator needs to evaluate its two operands, so it can not be used in this type of recursion.
Am I right ? and if yes, why is it like that ?
Thank you.
I'm going to start at the end of the question, then work up.
From what I understand, the ternary operator needs to evaluate its two operands
No. The ternary (meaning "made of three") operator has three operands, not two. When evaluating this operator, two of the three operands are evaluated: the condition and whichever operand the condition picks.
Evaluation is not where your problem lies.
the first, using ternary operator, does not compile.
I think I see why this is. You are assigning the result of the conditional operator to a std::size_t. In order for this to compile, the type of this result must be std::size_t or convertible to that type. So the compiler needs to determine the type of the result. I found rules for determining the type. The first rule applies if either the second or third operands has type void. So even though one of these operands will not be evaluated, both of their types must be known.
OK, so what is the type of your third operand, the one that will not be evaluated? Well, it's 1 + index_of<int>, so we better check the declaration of index_of<int>. Oops, we need two parameters. Cue error message.
This is probably something you'd have to deal with anyway, as you should get the same error for either approach in the "not found" case (for example: index_of<unsigned, int, long, float>). As you may have noticed, the default error message does not do a good job describing what went wrong, so it's probably a good idea for your template to specifically address that case, even if addressing that case just means providing a more understandable compiler error.
From what I understand, the ternary operator needs to evaluate its two operands, so it can not be used in this type of recursion.
Not evaluation, it's instantiation. When the expression std::is_same_v<Searching, First> ? 0 : 1 + index_of<Searching, Others...> is instantiated, all the three operands must be instantiated (not evaluated), so an error occurs due to the instantiation of index_of<Searching, Others...>. It is analogous to the difference between if and if constexpr. If you change the if constexpr in your second way to if, it does not compile too.
A workaround is to use your second way (i.e. function template) to initialize index_of, like
template<typename Searching, typename First, typename... Others>
constexpr std::size_t IndexOf() {
if constexpr(std::is_same_v<Searching,First>)
return 0;
else return 1 + IndexOf<Searching,Others...>();
};
template<typename Searching, typename First, typename...Others>
constexpr std::size_t index_of = IndexOf<Searching, First, Others...>();
or use template specialization:
template<typename Searching, typename First, typename... Others>
constexpr std::size_t index_of = 1 + index_of<Searching, Others...>;
template<typename Searching, typename... Others>
constexpr std::size_t index_of<Searching, Searching, Others...> = 0;
If you want a clearer error message, you can wrap the variable template in a class and use static_assert.
Related
Is it possible to produce a compile-time boolean value based on whether or not a C++11 expression is a constant expression (i.e. constexpr) in C++11? A few questions on SO relate to this, but I don't see a straight answer anywhere.
I once wrote it (EDIT: see below for limitations and explanations). From https://stackoverflow.com/a/10287598/34509 :
template<typename T>
constexpr typename remove_reference<T>::type makeprval(T && t) {
return t;
}
#define isprvalconstexpr(e) noexcept(makeprval(e))
However there are many kinds of constant expressions. The above answer detects prvalue constant expressions.
Explanation
The noexcept(e) expression gives false iff e contains
a potentially evaluated call to a function that does not have a non-throwing exception-specification unless the call is a constant expression,
a potentially evaluated throw expression,
a potentially evaluated throwable form of dynamic_cast or typeid.
Note that the function template makeprval is not declared noexcept, so the call needs to be a constant expression for the first bullet not to apply, and this is what we abuse. We need the other bullets to not apply aswell, but thanksfully, both a throw and a throwable dynamic_cast or typeid aren't allowed in constant expressions aswell, so this is fine.
Limitations
Unfortunately there is a subtle limitation, which may or may not matter for you. The notion of "potentially evaluated" is much more conservative than the limits of what constant expressions apply. So the above noexcept may give false negatives. It will report that some expressions aren't prvalue constant expressions, even though they are. Example:
constexpr int a = (0 ? throw "fooled!" : 42);
constexpr bool atest = isprvalconstexpr((0 ? throw "fooled!" : 42));
In the above atest is false, even though the initialization of a succeeded. That is because for being a constant expression, it suffices that the "evil" non-constant sub-expressions are "never evaluated", even though those evil sub-expressions are potentially-evaluated, formally.
As of 2017, is_constexpr is not possible in C++11. That sounds like an odd thing to say, so let me explain a bit of the history.
First, we added this feature to resolve a defect: http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1129
Johannes Schaub - litb posted a constexpr detection macro that relied on the provision that constant expressions are implicitly noexcept. This worked in C++11, but was never implemented by at least some compilers (for instance, clang). Then, as part of C++17, we evaluated Removing Deprecated Exception Specifications from C++17. As a side-effect of that wording, we accidentally removed that provision. When the Core Working Group discussed adding the provision back in, they realized that there were some serious problems with doing so. You can see the full details in the LLVM bug report. So rather than adding it back in, we decided to consider it a defect against all versions of standard and retroactively removed it.
The effect of this is that there is, to my knowledge, no way to detect whether an expression is usable as a constant expression.
Yes, this is possible. One way to do it (which is valid even with the recent noexcept changes) is to take advantage of the C++11 narrowing conversion rules:
A narrowing conversion is an implicit conversion [...] from an integer type or unscoped enumeration type to an integer type that cannot represent all the values of the original type, except where the source is a constant expression whose value after integral promotions will fit into the target type.
(emphasis mine). List initialization generally disallows narrowing conversions, and when combined with SFINAE we can build gadgets for detecting whether an arbitrary expression is a constant expression:
// p() here could be anything
template<int (*p)()> std::true_type is_constexpr_impl(decltype(int{(p(), 0U)}));
template<int (*p)()> std::false_type is_constexpr_impl(...);
template<int (*p)()> using is_constexpr = decltype(is_constexpr_impl<p>(0));
constexpr int f() { return 0; }
int g() { return 0; }
static_assert(is_constexpr<f>());
static_assert(!is_constexpr<g>());
Live demonstration.
The key here is that int{(expr, 0U)} contains a narrowing conversion from unsigned int to int (and thus is ill-formed), unless expr is a constant expression, in which case the entire expression (expr, 0U) is a constant expression whose evaluated value fits into the type int.
The following is an implementation of is_constexpr for functions, not for arbitrary expressions, for C++11 and C++17. It requires the arguments to the function you want to test to be default constructible, though.
#include <type_traits>
struct A {}; // don't make it too easy, use a UDT
A f1(A a) { return a; } // is_constexpr -> false
constexpr A f2(A a) { return a; } // is_constexpr -> true
// The following turns anything (in our case a value of A) into an int.
// This is necessary because non-type template arguments must be integral
// (likely to change with C++20).
template <class T> constexpr int make_int(T &&) { return 0; }
// Helper to turn some function type (e.g. int(float)) into a function
// pointer type (e.g. int (*)(float)).
template <class T> struct signature_from;
template <class R, class... Args> struct signature_from<R(Args...)> {
using type = R(*)(Args...);
};
// See std::void_t for the idea. This does it for ints instead of types.
template <int...> using void_from_int = void;
// The fallback case: F is not a function pointer to a constexpr function
template <class T, typename signature_from<T>::type F, class = void_from_int<>>
struct is_constexpr {
static constexpr bool value = false;
};
// If void_from_int<make_int(F(Args()...))> doesn't lead to a substitution
// failure, then this is the preferred specialization. In that case F must
// be a function pointer to a constexpr function. If it is not, it could
// not be used in a template argument.
template <class R, class... Args, typename signature_from<R(Args...)>::type F>
struct is_constexpr<R(Args...), F, void_from_int<make_int(F(Args()...))>>
{
static constexpr bool value = true;
};
// proof that it works:
static_assert(!is_constexpr<A(A), f1>::value, "");
static_assert( is_constexpr<A(A), f2>::value, "");
#if __cplusplus >= 201703
// with C++17 the type of the function can be deduced:
template<auto F> struct is_constexpr2 : is_constexpr<std::remove_pointer_t<decltype(F)>, F> {};
static_assert(!is_constexpr2<f1>::value, "");
static_assert( is_constexpr2<f2>::value, "");
#endif
See it in action at https://godbolt.org/g/rdeQme.
C++20 added std::is_constant_evaluated()
This allows checking if a certain expression is a constant evaluated expression, i.e. being evaluated at compile time.
Usage example:
constexpr int foo(int num) {
// below is true in case the condition is being evaluated at compile time
// side note, using: if constexpr (std::is_constant_evaluated())
// would be evaluated always to true, so you should use a simple if!
if (std::is_constant_evaluated()) {
return foo_compiletime(num);
}
else {
return foo_runtime(num);
}
}
int main() {
constexpr auto t1 = foo(6); // reaches foo_compiletime
const auto t2 = foo(6); // reaches foo_compiletime
int n = rand() % 10;
const auto t3 = foo(n); // reaches foo_runtime
auto t4 = foo(6); // unfortunately, reaches foo_runtime
}
The last call in the example above would reach foo_runtime, since the call is not within a constant expression context (the result is not being used as a constant expression, see also this SO answer).
This may lead to undesired pessimization, compared to the case of leaving the decision to the user, who may call:
auto t4 = foo_compiletime(6);
And the compiler is allowed to perform the operations inside foo_compiletime at compile time, if it is declared as constexpr function, or would be obliged to do that if it is declared consteval. However, once we leave the decision to the compiler, we will reach foo_runtime, unless we explicitly direct the compiler to go for foo_compiletime, by taking the result into a const, constexpr or constinit variable. Which then, in a way, omits the value of having one function for both scenarios, if the user is required to help the compiler peek the right path.
Another possible option for the call to be optimized, is:
constexpr auto temp = foo(6); // foo_compiletime
auto t4 = temp;
But again, we require the user to be aware of the inner behavior of foo, which is not exactly what we want to achieve.
See the pessimization in this code.
See more on that in this great blog post on the subject.
Edit, in order to avoid confusion: decltype does not accept two arguments. See answers.
The following two structs can be used to check for the existance of a member function on a type T during compile-time:
// Non-templated helper struct:
struct _test_has_foo {
template<class T>
static auto test(T* p) -> decltype(p->foo(), std::true_type());
template<class>
static auto test(...) -> std::false_type;
};
// Templated actual struct:
template<class T>
struct has_foo : decltype(_test_has_foo::test<T>(0))
{};
I think the idea is to use SFINAE when checking for the existance of a member function, so in case p->foo() isn't valid, only the ellipses version of test, which returns the std::false_type is defined. Otherwise the first method is defined for T* and will return std::true_type. The actual "switch" happens in the second class, which inherits from the type returned by test. This seems clever and "lightweight" compared to different approaches with is_same and stuff like that.
The decltype with two arguments first looked surprising to me, as I thought it just gets the type of an expression. When I saw the code above, I thought it's something like "try to compile the expressions and always return the type of the second. Fail if the expressions fail to compile" (so hide this specialization; SFINAE).
But:
Then I thought I could use this method to write any "is valid expression" checker, as long as it depends on some type T. Example:
...
template<class T>
static auto test(T* p) -> decltype(bar(*p), std::true_type());
...
http://ideone.com/dJkLPF
This, so I thought, will return a std::true_type if and only if bar is defined accepting a T as the first parameter (or if T is convertible, etc...), i.e.: if bar(*p) would compile if it was written in some context where p is defined of type T*.
However, the modification above evaluates always to std::false_type. Why is this? I don't want to fix it with some complicated different code. I just want to know why it doesn't work as I expected it to. Clearly, decltype with two arguments works different than I thought. I couldn't find any documentation; it's only explained with one expression everywhere.
It's an comma-separated list of expressions, the type is identical to the type of the last expression in the list. It's usually used to verify that the first expression is valid (compilable, think SFINAE), the second is used to specify that decltype should return in case the first expression is valid.
decltype does not take two arguments. Simply, it can can have an expression as its argument, and the comma operator is one way of creating expressions. Per Paragraph 5.18/1:
[...] A pair of expressions separated by a comma is evaluated left-to-right; the left expression is a discarded-value
expression (Clause 5). Every value computation and side effect associated with the left expression
is sequenced before every value computation and side effect associated with the right expression. The type
and value of the result are the type and value of the right operand; the result is of the same value category
as its right operand, and is a bit-field if its right operand is a glvalue and a bit-field. If the value of the right
operand is a temporary (12.2), the result is that temporary.
Therefore:
static_assert(std::is_same<decltype(42, 3.14), double>::value, "Will not fire");
This is a retelling of my previous post, since I changed the question (so it probably didn't get flagged as a new question and was missed). I'll hopefully trim it down too.
I had functions like:
#include <cstddef>
#include <type_traits>
template < typename E, typename T >
inline constexpr
auto checked_slice( E &&, T &&t ) noexcept -> T &&
{ return static_cast<T &&>(t); }
template < typename E, typename T, std::size_t N, typename U, typename ...V >
inline constexpr
auto checked_slice( E &&e, T (&t)[N], U &&u, V &&...v )
-> typename remove_some_extents<T, sizeof...(V)>::type &
{
typedef typename std::remove_reference<U>::type u_type;
typedef typename std::common_type<u_type, std::size_t>::type cmp_type;
return ( u < u_type{} ) || ( static_cast<cmp_type>(u) >=
static_cast<cmp_type>(N) ) ? throw e : checked_slice( static_cast<E &&>(e),
t[static_cast<U &&>( u )], static_cast<V &&>(v)... );
}
where remove_some_extents is a custom class template that's like calling the std::remove_extent meta-function a given number of times.
When I tried running the program, I got a bunch of errors like: "invalid initialization of reference of type Whatever(&)[X][Y] from expression of type Whatever(*)[Y]" (or Whatever(&)[Z] from Whatever*). My workaround was to convert the conditional expression to an if-else pair (and removing the constexpr).
I'm trying to figure out what's wrong, so I'm poking around the section about the conditional operator in the C++ (2011) standard. That's section 5.16. When one of the two possible actions is a throw command (or is otherwise a void expression), then the conditional has the type of the other expression, but the standard conversions, including array-to-pointer, is applied to that other expression. (This is in paragraph 2.) I think that's what's messing me up. Is there any way around it? I thought returning an array reference suppresses the a-to-p conversion. Why does it work when made into an if/else?
Your analysis is correct. I suspect that the non-void operand is 'decayed' (that is, the usual conversions are performed) in such a situation so as to mimic what happens when the two operands differ in types -- in the latter case more often than not the whole conditional expression is a prvalue.
One situation in which we know for sure both value category and type of a conditional expression is when the two operands are exact matches, so we can use that to our advantage:
cond ? (throw e, t) : t
will be an lvalue of array reference type. (Of course the last operand doesn't have to be literally t -- you can plug your recursive call here just fine.)
You did not encounter any such hurdle when using if/else because as a statement the language does not have to specify a common type and value category for it.
Edit, in order to avoid confusion: decltype does not accept two arguments. See answers.
The following two structs can be used to check for the existance of a member function on a type T during compile-time:
// Non-templated helper struct:
struct _test_has_foo {
template<class T>
static auto test(T* p) -> decltype(p->foo(), std::true_type());
template<class>
static auto test(...) -> std::false_type;
};
// Templated actual struct:
template<class T>
struct has_foo : decltype(_test_has_foo::test<T>(0))
{};
I think the idea is to use SFINAE when checking for the existance of a member function, so in case p->foo() isn't valid, only the ellipses version of test, which returns the std::false_type is defined. Otherwise the first method is defined for T* and will return std::true_type. The actual "switch" happens in the second class, which inherits from the type returned by test. This seems clever and "lightweight" compared to different approaches with is_same and stuff like that.
The decltype with two arguments first looked surprising to me, as I thought it just gets the type of an expression. When I saw the code above, I thought it's something like "try to compile the expressions and always return the type of the second. Fail if the expressions fail to compile" (so hide this specialization; SFINAE).
But:
Then I thought I could use this method to write any "is valid expression" checker, as long as it depends on some type T. Example:
...
template<class T>
static auto test(T* p) -> decltype(bar(*p), std::true_type());
...
http://ideone.com/dJkLPF
This, so I thought, will return a std::true_type if and only if bar is defined accepting a T as the first parameter (or if T is convertible, etc...), i.e.: if bar(*p) would compile if it was written in some context where p is defined of type T*.
However, the modification above evaluates always to std::false_type. Why is this? I don't want to fix it with some complicated different code. I just want to know why it doesn't work as I expected it to. Clearly, decltype with two arguments works different than I thought. I couldn't find any documentation; it's only explained with one expression everywhere.
It's an comma-separated list of expressions, the type is identical to the type of the last expression in the list. It's usually used to verify that the first expression is valid (compilable, think SFINAE), the second is used to specify that decltype should return in case the first expression is valid.
decltype does not take two arguments. Simply, it can can have an expression as its argument, and the comma operator is one way of creating expressions. Per Paragraph 5.18/1:
[...] A pair of expressions separated by a comma is evaluated left-to-right; the left expression is a discarded-value
expression (Clause 5). Every value computation and side effect associated with the left expression
is sequenced before every value computation and side effect associated with the right expression. The type
and value of the result are the type and value of the right operand; the result is of the same value category
as its right operand, and is a bit-field if its right operand is a glvalue and a bit-field. If the value of the right
operand is a temporary (12.2), the result is that temporary.
Therefore:
static_assert(std::is_same<decltype(42, 3.14), double>::value, "Will not fire");
Is it possible to produce a compile-time boolean value based on whether or not a C++11 expression is a constant expression (i.e. constexpr) in C++11? A few questions on SO relate to this, but I don't see a straight answer anywhere.
I once wrote it (EDIT: see below for limitations and explanations). From https://stackoverflow.com/a/10287598/34509 :
template<typename T>
constexpr typename remove_reference<T>::type makeprval(T && t) {
return t;
}
#define isprvalconstexpr(e) noexcept(makeprval(e))
However there are many kinds of constant expressions. The above answer detects prvalue constant expressions.
Explanation
The noexcept(e) expression gives false iff e contains
a potentially evaluated call to a function that does not have a non-throwing exception-specification unless the call is a constant expression,
a potentially evaluated throw expression,
a potentially evaluated throwable form of dynamic_cast or typeid.
Note that the function template makeprval is not declared noexcept, so the call needs to be a constant expression for the first bullet not to apply, and this is what we abuse. We need the other bullets to not apply aswell, but thanksfully, both a throw and a throwable dynamic_cast or typeid aren't allowed in constant expressions aswell, so this is fine.
Limitations
Unfortunately there is a subtle limitation, which may or may not matter for you. The notion of "potentially evaluated" is much more conservative than the limits of what constant expressions apply. So the above noexcept may give false negatives. It will report that some expressions aren't prvalue constant expressions, even though they are. Example:
constexpr int a = (0 ? throw "fooled!" : 42);
constexpr bool atest = isprvalconstexpr((0 ? throw "fooled!" : 42));
In the above atest is false, even though the initialization of a succeeded. That is because for being a constant expression, it suffices that the "evil" non-constant sub-expressions are "never evaluated", even though those evil sub-expressions are potentially-evaluated, formally.
As of 2017, is_constexpr is not possible in C++11. That sounds like an odd thing to say, so let me explain a bit of the history.
First, we added this feature to resolve a defect: http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1129
Johannes Schaub - litb posted a constexpr detection macro that relied on the provision that constant expressions are implicitly noexcept. This worked in C++11, but was never implemented by at least some compilers (for instance, clang). Then, as part of C++17, we evaluated Removing Deprecated Exception Specifications from C++17. As a side-effect of that wording, we accidentally removed that provision. When the Core Working Group discussed adding the provision back in, they realized that there were some serious problems with doing so. You can see the full details in the LLVM bug report. So rather than adding it back in, we decided to consider it a defect against all versions of standard and retroactively removed it.
The effect of this is that there is, to my knowledge, no way to detect whether an expression is usable as a constant expression.
Yes, this is possible. One way to do it (which is valid even with the recent noexcept changes) is to take advantage of the C++11 narrowing conversion rules:
A narrowing conversion is an implicit conversion [...] from an integer type or unscoped enumeration type to an integer type that cannot represent all the values of the original type, except where the source is a constant expression whose value after integral promotions will fit into the target type.
(emphasis mine). List initialization generally disallows narrowing conversions, and when combined with SFINAE we can build gadgets for detecting whether an arbitrary expression is a constant expression:
// p() here could be anything
template<int (*p)()> std::true_type is_constexpr_impl(decltype(int{(p(), 0U)}));
template<int (*p)()> std::false_type is_constexpr_impl(...);
template<int (*p)()> using is_constexpr = decltype(is_constexpr_impl<p>(0));
constexpr int f() { return 0; }
int g() { return 0; }
static_assert(is_constexpr<f>());
static_assert(!is_constexpr<g>());
Live demonstration.
The key here is that int{(expr, 0U)} contains a narrowing conversion from unsigned int to int (and thus is ill-formed), unless expr is a constant expression, in which case the entire expression (expr, 0U) is a constant expression whose evaluated value fits into the type int.
The following is an implementation of is_constexpr for functions, not for arbitrary expressions, for C++11 and C++17. It requires the arguments to the function you want to test to be default constructible, though.
#include <type_traits>
struct A {}; // don't make it too easy, use a UDT
A f1(A a) { return a; } // is_constexpr -> false
constexpr A f2(A a) { return a; } // is_constexpr -> true
// The following turns anything (in our case a value of A) into an int.
// This is necessary because non-type template arguments must be integral
// (likely to change with C++20).
template <class T> constexpr int make_int(T &&) { return 0; }
// Helper to turn some function type (e.g. int(float)) into a function
// pointer type (e.g. int (*)(float)).
template <class T> struct signature_from;
template <class R, class... Args> struct signature_from<R(Args...)> {
using type = R(*)(Args...);
};
// See std::void_t for the idea. This does it for ints instead of types.
template <int...> using void_from_int = void;
// The fallback case: F is not a function pointer to a constexpr function
template <class T, typename signature_from<T>::type F, class = void_from_int<>>
struct is_constexpr {
static constexpr bool value = false;
};
// If void_from_int<make_int(F(Args()...))> doesn't lead to a substitution
// failure, then this is the preferred specialization. In that case F must
// be a function pointer to a constexpr function. If it is not, it could
// not be used in a template argument.
template <class R, class... Args, typename signature_from<R(Args...)>::type F>
struct is_constexpr<R(Args...), F, void_from_int<make_int(F(Args()...))>>
{
static constexpr bool value = true;
};
// proof that it works:
static_assert(!is_constexpr<A(A), f1>::value, "");
static_assert( is_constexpr<A(A), f2>::value, "");
#if __cplusplus >= 201703
// with C++17 the type of the function can be deduced:
template<auto F> struct is_constexpr2 : is_constexpr<std::remove_pointer_t<decltype(F)>, F> {};
static_assert(!is_constexpr2<f1>::value, "");
static_assert( is_constexpr2<f2>::value, "");
#endif
See it in action at https://godbolt.org/g/rdeQme.
C++20 added std::is_constant_evaluated()
This allows checking if a certain expression is a constant evaluated expression, i.e. being evaluated at compile time.
Usage example:
constexpr int foo(int num) {
// below is true in case the condition is being evaluated at compile time
// side note, using: if constexpr (std::is_constant_evaluated())
// would be evaluated always to true, so you should use a simple if!
if (std::is_constant_evaluated()) {
return foo_compiletime(num);
}
else {
return foo_runtime(num);
}
}
int main() {
constexpr auto t1 = foo(6); // reaches foo_compiletime
const auto t2 = foo(6); // reaches foo_compiletime
int n = rand() % 10;
const auto t3 = foo(n); // reaches foo_runtime
auto t4 = foo(6); // unfortunately, reaches foo_runtime
}
The last call in the example above would reach foo_runtime, since the call is not within a constant expression context (the result is not being used as a constant expression, see also this SO answer).
This may lead to undesired pessimization, compared to the case of leaving the decision to the user, who may call:
auto t4 = foo_compiletime(6);
And the compiler is allowed to perform the operations inside foo_compiletime at compile time, if it is declared as constexpr function, or would be obliged to do that if it is declared consteval. However, once we leave the decision to the compiler, we will reach foo_runtime, unless we explicitly direct the compiler to go for foo_compiletime, by taking the result into a const, constexpr or constinit variable. Which then, in a way, omits the value of having one function for both scenarios, if the user is required to help the compiler peek the right path.
Another possible option for the call to be optimized, is:
constexpr auto temp = foo(6); // foo_compiletime
auto t4 = temp;
But again, we require the user to be aware of the inner behavior of foo, which is not exactly what we want to achieve.
See the pessimization in this code.
See more on that in this great blog post on the subject.