Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

why is there a "never use non-literal type" rule in constexpr functions?

Tags:

c++

constexpr

Take the following legal code:

bool bar();

template <class T>
constexpr bool foo(T t) {
  if (t>0) {
    return true;
  }
  return bar();
}


int main() {
  //constexpr bool cb1 = foo(-1); // error as expected  because it would attempt to call bar()
  constexpr bool cb2 = foo(1); // ok
}

https://godbolt.org/z/UWt_3A

So, as long as we don't hit a non-constexpr code-path in a compile time evaluation context our constexpr is well formed. Neat!

However, if I apply the same practical notion, but happen to include a non-literal type in a conditional code-path, such as std::string, then the standard says no-no:

#include <string>

bool bar(std::string);

template <class T>
constexpr bool foo(T t) {
  if (t>0) {
    return true;
  }
  std::string s = "abc";
  return bar(s);
}


int main() {
  //constexpr bool cb1 = foo(-1); // error as expected
  constexpr bool cb2 = foo(1); // this is also an error now :(
}

https://godbolt.org/z/iHThCq

What is the rationale behind this? Why is using std::string illegal at all cost even though it never actually gets constructed (or destroyed)?

Bonus question: why is the following legal then: https://godbolt.org/z/L3np-u (slight variation on above, without defining std::string) ?!

like image 492
Nick Avatar asked Jul 07 '20 19:07

Nick


People also ask

What is the point of constexpr functions?

constexpr functions A constexpr function is one whose return value is computable at compile time when consuming code requires it. Consuming code requires the return value at compile time to initialize a constexpr variable, or to provide a non-type template argument.

Why is constexpr over const?

const can only be used with non-static member function whereas constexpr can be used with member and non-member functions, even with constructors but with condition that argument and return type must be of literal types.

Should I use constexpr or const?

const applies for variables, and prevents them from being modified in your code. constexpr tells the compiler that this expression results in a compile time constant value, so it can be used in places like array lengths, assigning to const variables, etc.

Can a constexpr function throw?

Even though try blocks and inline assembly are allowed in constexpr functions, throwing exceptions or executing the assembly is still disallowed in a constant expression.


2 Answers

I'm just guessing here, but can it be because being std::string s = "abc" an automatic variable and allocated in the stack at function begin (even if not yet constructed) breaks the constexpr rules?

If I change the code to:

using namespace std::string_literals;

bool bar(std::string);

template <class T>
constexpr bool foo(T t) {
    if (t>0) {
        return true;
    }
    else {
        //std::string ss = "abc"s;
        return bar("abc"s);
    }
    return false;
}

as there is no need to allocate anything it compiles.

I explain my reasoning (and response to comments) here, as I need more space than in a comment.

As @StoryTeller-UnslanderMonica says, "guessing is a bad basis for answering questions".

Absolutely yes. That's why I begin saying just that: I'm guessing. And that have a reason.

I don't like to guess normally but I found this interesting and want to throw a thought to see if someone says I'm wrong (something I'm pretty ready to accept.)

But going to the point, literal type variables are normally stored at some read only memory data segment (unless they are numbers, those can be translated directly to ASM MOV/... instructions), not at stack.

If declared automatic (storing at stack):

Storage duration

All objects in a program have one of the following storage durations:

automatic storage duration. The storage for the object is allocated at the beginning of the enclosing code block and deallocated at the end. All local objects have this storage duration, except those declared static, extern or thread_local.

(Emphasis mine.)

So, even if declared after the if, the storage is allocated and should be deallocated in any case (in the example shown by OP.)

In fact, if done like this:

template <class T>
constexpr bool foo(T t) {
    if (t>0) {
        return true;
    }
    const std::string ss = "abc"s;
    return bar(ss);
}

the error is:

main.cc:15:16: error: call to non-‘constexpr’ function ‘std::__cxx11::basic_string<char> std::literals::string_literals::operator""s(const char*, std::size_t)’

why? I guess because, being automatic, "the storage for the object is allocated at the beginning of the enclosing code block" (beginning of the function) no matter the execution code path.

Moreover, if you declare it constexpr, it introduces the destructor:

template <class T>
constexpr bool foo(T t) {
    if (t>0) {
        return true;
    }
    constexpr std::string ss = "abc"s;
    return bar(ss);
}

error:

main.cc:19:32: error: temporary of non-literal type ‘const string’ {aka ‘const std::__cxx11::basic_string<char>’} in a constant expression
     constexpr std::string ss = "abc"s;
                                ^~~~~~
In file included from /usr/include/c++/8/string:52,
                 from main.cc:2:
/usr/include/c++/8/bits/basic_string.h:77:11: note: ‘std::__cxx11::basic_string<char>’ is not literal because:
     class basic_string
           ^~~~~~~~~~~~
/usr/include/c++/8/bits/basic_string.h:77:11: note:   ‘std::__cxx11::basic_string<char>’ has a non-trivial destructor
main.cc: In instantiation of ‘constexpr bool foo(T) [with T = int]’:
main.cc:25:29:   required from here
main.cc:19:27: error: the type ‘const string’ {aka ‘const std::__cxx11::basic_string<char>’} of ‘constexpr’ variable ‘ss’ is not literal
     constexpr std::string ss = "abc"s;

I think the key is: ‘std::__cxx11::basic_string<char>’ has a non-trivial destructor.

so the theoretical call to the destructor is taken in account before the execution code path.

Why?

Because "the storage for the object is allocated at the beginning of the enclosing code block".

The following:

template <class T>
constexpr bool foo(T t) {
    if (t>0) {
        return true;
    }
    return bar("abc"s);
}

creates a temporary:

main.cc:19:15: error: call to non-‘constexpr’ function ‘bool bar(std::__cxx11::string)’
     return bar("abc"s);

but

template <class T>
constexpr bool foo(T t) {
    if (t>0) {
        return true;
    } else {
        return bar("abc"s);
    }
    return false;
}

creates the temporary only if the execution path goes to the else (which is not the case.)

As said, this is a guess, but I think a based guess, not just a blind try.

Again, I'm convinced this depends on compiler implementation. I'm by no means a C++ standard expert, but I haven't been able to find this explicit case in any document.

I've run the program in gdb to see if it enters in the foo function:

bool bar(std::string);

template <class T>
constexpr bool foo(T t) {
    if (t>0) {
        return true;
    } else {
        //std::string ss = "abc"s;
        return bar("abc"s);
    }
    return false;
}

int main() {
    //constexpr bool cb1 = foo(-1); // error as expected
    constexpr bool cb2 = foo(1); // this is also an error now :(

    cout << "Bool: " << cb2 << endl;
    
    return 0;
}

it links without bar being defined so...

manuel@desktop:~/projects$ g++ -Wall -Wextra -g main.cc -o main --std=gnu++2a -Wpedantic && time ./main
Bool: 1

real    0m0,002s
user    0m0,000s
sys 0m0,002s
manuel@desktop:~/projects$ gdb ./main
GNU gdb (Debian 8.2.1-2+b3) 8.2.1
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./main...done.
(gdb) b main
Breakpoint 1 at 0x117d: file main.cc, line 27.
(gdb) r
Starting program: /home/manuel/projects/main 

Breakpoint 1, main () at main.cc:27
27      constexpr bool cb2 = foo(1); // this is also an error now :(
(gdb) s
29      cout << "Bool: " << cb2 << endl;
(gdb) s
Bool: 1
31      return 0;
(gdb) s
32  }
(gdb) q
A debugging session is active.

    Inferior 1 [process 18799] will be killed.

Quit anyway? (y or n) y
like image 191
Manuel Avatar answered Oct 19 '22 03:10

Manuel


Because your second code violates the following rule:

The definition of a constexpr function shall satisfy the following requirements:

  • its function-body shall be = delete, = default, or a compound-statement that does not contain
  • a definition of a variable of non-literal type or of static or thread storage duration or for which no initialization is performed.

Since in your second code, you defined a variable like std::string s = "abc";, thereof, the type std::string is a non-literal type, hence that violates the rule I cited. So, your second code is ill-formed.

UPDATE:

why the rule is necessary to exist? because the following rule:

A constexpr specifier used in an object declaration declares the object as const. Such an object shall have literal type and shall be initialized. In any constexpr variable declaration, the full-expression of the initialization shall be a constant expression.

The full-expression of the initialization include the invocation of destructor, due to this rule:

A full-expression is

  • an invocation of a destructor generated at the end of the lifetime of an object other than a temporary object, or

Because std::string s = "abc"; is a variable with automatic duration storage, It's not a temporary object, hence the invocation of its destructor be included within the full-expression of the initialization. Because the destructor of non-literal type wouldn't be a constexpr function, So this is why the rule is necessary.

like image 41
xmh0511 Avatar answered Oct 19 '22 02:10

xmh0511