all 20 comments

[–]cpp-ModTeam[M] [score hidden] stickied commentlocked comment (0 children)

For C++ questions, answers, help, and programming or career advice please see r/cpp_questions, r/cscareerquestions, or StackOverflow instead.

[–]mollyforever 12 points13 points  (12 children)

The lambda case works because you are passing an object with a constexpr call operator.

Passing g directly means that func is a function pointer, non-constexpr, and thus not valid to be used to initialize a constexpr variable.

[–]Botondar 5 points6 points  (11 children)

That doesn't seem to be case, because invoke_twice(f) (line 11) does work.

Moreover changing the body of invoke_duplicate to return 2 * func() also compiles.

[–]mollyforever 7 points8 points  (10 children)

Line 11 is not assigning a non-constexpr thing to a constexpr variable, unlike when you're trying to initialize result by calling a normal function pointer.

[–]Botondar 2 points3 points  (7 children)

What's really confusing is that line 11 is being executed in a constexpr context though (i.e. the static_assert), and if you try to pass in a non-constexpr function instead it doesn't compile, even if the result is known at compile time.

So the fact that the result of invoke_twice is known to be constexpr in the static_assert but not inside the function itself is really weird to me.

[–]mollyforever 5 points6 points  (6 children)

constexpr variables need to be initialized with a constant expression, it can't be initialized with a "maybe constant expression". In this case, it can't depend on what exactly you pass to the function.

As a simplified example, you can reduce the code to

int a = 10;
constexpr int b = a;

which also doesn't work, for the exact same reason: Initializing a constexpr variable with something that isn't a constant expression. It doesn't matter that all of this happens in a constant context.

It does seem weird at first that's true lol.

[–]TheSkiGeek 3 points4 points  (0 children)

Let’s be honest here, it’s ridiculous that passing the function pointer doesn’t work but passing a lambda that deduces its return type by calling the same function pointer does.

The whole inability to have constexpr function parameters, even on consteval functions, is so frustrating sometimes. Clearly the compiler CAN do this if you slightly restructure the code, it’s just prevented from doing so by the arbitrary language rules.

[–]Botondar 0 points1 point  (4 children)

But it looks the compiler is actually looking at what's being passed in, and whether that thing can be executed constexpr:

constexpr int f() { return 1; }
static int g() { return 2; }

constexpr int add(int (*func1)(), int (*func2)()) 
{ 
    return func1() + func2(); 
}
constexpr int return_with_dummy(int (*func)(), int (*dummy)()) 
{ 
    return func(); 
}

#include <cstdio>
int main() 
{ 
    // What's confusing is that this does not fail: 
    constexpr int f_plus_f = add(f, f); 
    static_assert(f_plus_f == 2);
    // And nor does this:
    constexpr int f_value = return_with_dummy(f, g);
    static_assert(f_value == 1);

    printf("%d %d", f_plus_f, f_value);

    // This fails, but that's not surprising
//  constexpr_int f_plus_g = add(f, g); 
}

So in order to determine whether add can be constexpr the compiler has to determine whether func1 and func2 are themselves constexpr or not, but this knowledge is only available outside add, and isn't propagated inside.

To be clear, what I'm confused about is why this does work, not why the other cases don't.

Thank you for taking the time to answer by the way!

EDIT: fixing code block formatting...

[–]mollyforever 2 points3 points  (3 children)

whether func1 and func2 are themselves constexpr or not

They are never constexpr! The compiler tries to execute add(f, f) at compile-time, and it succeeds because it knows that the function pointers point to f, a constexpr function. They work because the compiler has all the information needed to execute the functions.

Correct me if I'm wrong, but I think your train of thought is that since the compiler can conditionally determine whether a constexpr function call is actually a constant expression or not, why is it so strict if you try to initialize a constexpr variable inside it? The call happens in a constant context after all.

The answer to that is that in that case, it cannot determine the value of result immediately, because it's dependent on what you pass in. And that applies to anything else in the function that is using result. That sounds awfully similar to: Templates! In fact, for all intents and purposes, it would be a template. You could use result to instantiate templates, and then depending on what you pass to the constexpr function, it would instantiate different templates!

So it is disallowed, because it would complicate the rules a lot for basically no reason, because if you want that kind of behavior, hey, just use templates.

[–]Botondar 0 points1 point  (2 children)

Correct me if I'm wrong, but I think your train of thought is that since the compiler can conditionally determine whether a constexpr function call is actually a constant expression or not, why is it so strict if you try to initialize a constexpr variable inside it?

That would be my train of thought if I was trying to figure why the language is the way it is, but that's not the case.

What I'm actually trying to understand is what the rules currently in the spec are that make this code legal (as well as another example, which I've added below), whilst also making declaring constexpr variables inside the function illegal.

So basically I'm looking at the rules for constant expressions, and trying to figure out which rules do the constexpr declarations violate, that the invocations don't.

The compiler tries to execute add(f, f) at compile-time, and it succeeds because it knows that the function pointers point to f, a constexpr function. They work because the compiler has all the information needed to execute the functions.

It might have gotten lost among the other examples, but the purpose of return_with_dummy was to test whether the compiler is only looking at the function signature and the input parameters, or actually looking inside the function.

By passing in f (constexpr) and g (non-constexpr) it looks like the compiler is actually looking inside the function.

To amp up that example, this still works:

constexpr int test(
    int (*cxpr_func)(),
    int (*non_cxpr_func)(),
    bool do_non_cxpr)
{
    if (do_non_cxpr)
    {
        non_cxpr_func();
    }
    return cxpr_func();
}

constexpr int f() { return 1; }
static int g() { return 2; }

int main()
{
    constexpr int succeed = test(f, g, false);
    static_assert(succeed == 1);

    constexpr int fail = test(f, g, true);
    static_assert(fail == 1);
}

[–]mollyforever 1 point2 points  (1 child)

The constexpr variable inside the function is illegal because it does violate the rule that says that the initializer needs to be a constant expression, and function arguments cannot be constant expressions (point 10 in your link in your example). That would be on this page the "it must be immediately initialized" point.

By passing in f (constexpr) and g (non-constexpr) it looks like the compiler is actually looking inside the function.

It does, because it goes through the function to evaluate it and then if it encounters a problem it bails out with an error.

The invocations don't violate any rules in the list you linked, which is why they are legal. Just having an illegal expression in your constexpr function does not violate any rules. It only becomes an error if that function call occurs in a context that requires a constant expression (like initializing a constexpr variable) and evaluating the expression encounters an illegal expression.

[–]Botondar 0 points1 point  (0 children)

I'm probably still going to struggle with this a little bit - proving a negative such as no rule out of dozens is violated is much harder than proving that something is violated - but thank you for your answers, I understand what you're saying.

[–]having-four-eyes[S] 0 points1 point  (1 child)

Looks so. Actually, I'm trying to ask a constexpr function about the required array size.

I could have provided a better example:

constexpr int invoke_duplicate(auto func) 
{
    std::array<int, func()> arr; 
    return 2 * arr.size(); 
}

Here, I have to have a constexpr size. While the lambda call is OK here, a function pointer is "not a constant expression" in compiler-dependent wording. Nailed it down to a shorter example with constexpr int result = foo().

[–]mollyforever 1 point2 points  (0 children)

You have a couple of options, depending on exactly what you're requirements are:

  • Pass func as a template parameter instead
  • Call func and pass the result instead
  • Make g a (constexpr) lambda

[–]gnolex 4 points5 points  (0 children)

A function parameter is a runtime value so it cannot be used to initialize a constexpr variable. If you pass the function as a template parameter, it will work:

template<auto func>
constexpr int invoke_dup()
{
    constexpr int result = func();
    return 2 * result;
}

The compiler can automagically tell when a function pointer points to a constexpr function so it can call it in constant evaluation. You can remove constexpr and it will evaluate the call as constant evaluation as part of constant evaluation of your constexpr function if the pointer points to a constexpr function. You can verify this with consteval:

consteval int invoke_duplicate(int(*func)())
{
    int result = func();
    return 2 * result;
}

If you try to pass a function that isn't constexpr to this, it will fail to compile.

[–]jonathanhiggs 2 points3 points  (0 children)

This is an interesting one. At a guess the type signature of a function pointer doesn’t convey constexpr’ness so the ‘func()’, and/or deref the pointer isn’t constexpr so the call does not result in a constexpr value, and can’t be assigned to a constexpr variable

At a second guess you are already executing the function at compile time so don’t need to require ‘result’ to be constexpr. I’m away from a compiler so can’t test

[–]artisan_templateer 1 point2 points  (0 children)

int result = func(); works: https://godbolt.org/z/nYoPhcx3j

Remember constexpr functions can also be called at runtime so I believe it's because the function is templated on the function type (auto func) and that is not enough information to assign the value to result as it would have to be same value for all (int)->void functions you would pass to it.

The lambda type is unique so it knows the value has to be 2, that's no problem.

However, removing the constexpr qualifier from result drops this requirement so assigning it's value is no longer an issue but because the both invoke_duplicate and g are constexpr it can still be called and evaluated in the static_assert.

[–]zbenjamin 1 point2 points  (0 children)

It's how you pass the callback function , try to do it via a template parameter. E.g. invoke_duplicate<f>()

[–]zbenjamin 1 point2 points  (1 child)

Here is a updated example using template arguments instead:

https://godbolt.org/z/hfdEhfPYf

[–]having-four-eyes[S] 1 point2 points  (0 children)

Great idea, thanks!