Объявление строки во время компиляции
#include <iostream>
#include <string>
constexpr auto get_str() {
return std::string{"Hello, world!"};
}
int main() {
constexpr auto str = get_str();
std::cout << str << '\n';
}
Почему код не компилируется?
error: 'std::__cxx11::basic_string<char>{std::__cxx11::basic_string<char>::_Alloc_hider{((char*)(& str.std::__cxx11::basic_string<char>::<anonymous>.std::__cxx11::basic_string<char>::<unnamed union>::_M_local_buf))}, 13, std::__cxx11::basic_string<char>::<unnamed union>{char [16]{'H', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', 0}}}' is not a constant expression
Если не объявлять переменную, то всё ок:
#include <iostream>
#include <string>
constexpr auto get_str() {
return std::string{"Hello, world!"};
}
int main() {
std::cout << get_str() << '\n';
}
Ответы (2 шт):
Как подсказывает ошибка компилятора, чтобы объявить переменную str со спецификатором constexpr, надо инициализировать её с помощью constant expression.
... In any constexpr variable declaration, the full-expression of the initialization shall be a constant expression. ...
Однако выражение get_str() не является core constant expression (ниже рассмотрим подробно). Т.к. выражение не является core constant expression, то оно не является и constant expression.
A constant expression is either a glvalue core constant expression that refers to an entity that is a permitted result of a constant expression (as defined below), or a prvalue core constant expression whose value satisfies the following constraints:
- ...
Чтобы показать, что get_str() не является core constant expression, для начала заметим, что в данном случае оно эквивалентно выражению std::string{"Hello, world!"}.
The result of a function call is the result of the possibly-converted operand of the return statement that transferred control out of the called function (if any),
except in a virtual function call if the return type of the final overrider is different from the return type of the statically chosen function, the value returned from the final overrider is converted to the return type of the statically chosen function.
Это выражение эквивалентно std::basic_string<char, std::char_traits<char>, std::allocator<char>>{"Hello, world!"}.
...
namespace std {
template<class charT, class traits = char_traits<charT>, class Allocator = allocator<charT>>
class basic_string;
...
using string = basic_string<char>;
...
}
Оно вызывает std::allocator_traits<std::allocator<U>>::rebind_traits<char>::construct для некоторого типа U, определённого ниже (по крайней мере, as if).
...
constexpr basic_string(const charT* s, const Allocator& a = Allocator());
...
... Every object of type
basic_string<charT, traits, Allocator>uses an object of typeAllocatorto allocate and free storage for the containedcharTobjects as needed. TheAllocatorobject used is obtained as described in [container.requirements.general]. ...
[container.requirements.general]/3:
For the components affected by this subclause that declare an
allocator_type, objects stored in these components shall be constructed using the functionallocator_traits<allocator_type>::rebind_traits<U>::constructand destroyed using the functionallocator_traits<allocator_type>::rebind_traits<U>::destroy, whereUis eitherallocator_type::value_typeor an internal type used by the container.
Это эквивалентно std::allocator<U>::construct.
...
namespace std {
...
template<class T> using rebind_alloc = *see below*;
template<class T> using rebind_traits = allocator_traits<rebind_alloc<T>>;
...
}
template<class T> using rebind_alloc= see below;Alias template:
Alloc::rebind<T>::otherif the qualified-idAlloc::rebind<T>::otheris valid and denotes a type ([temp.deduct]); otherwise,Alloc<T, Args>ifAllocis a class template instantiation of the formAlloc<U, Args>, whereArgsis zero or more type arguments; otherwise, the instantiation ofrebind_allocis ill-formed.
Таким образом, get_str() не является core constant expression, т.к. срабатывает следующий пункт стандарта (память не освобождается во время constant evaluation, т.к. она используется для инициализации str, которая продолжает свой lifetime до конца блока):
An expression E is a core constant expression unless the evaluation of E, following the rules of the abstract machine, would evaluate one of the following :
- ...
- a call to an instance of
std::allocator<T>::allocate([allocator.members]),unless the allocated storage is deallocated within the evaluation of E;- ...
Возможна ли ситуация, когда память освобождается до конца константного выражения? — Да, например, в следующем коде:
#include <string>
constexpr auto get_str() {
return std::string{"Hello, world!"};
}
static_assert(std::size(get_str()));
Значит, сама по себе функция well-formed.
For a constexpr function or constexpr constructor that is neither defaulted nor a template, if no argument values exist such that an invocation of the function or constructor could be an evaluated subexpression of a core constant expression, or, for a constructor, an evaluated subexpression of the initialization full-expression of some constant-initialized object ([basic.start.static]), the program is ill-formed, no diagnostic required. ...
Поэтому второй пример из вопроса валиден — там constexpr функция используется не в константном выражении.
An invocation of a constexpr function in a given context produces the same result as an invocation of an equivalent non-constexpr function in the same context in all respects except that
- an invocation of a constexpr function can appear in a constant expression ([expr.const]) and
- copy elision is not performed in a constant expression ([class.copy.elision]).
...
Что делать, если мы таки хотим иметь constexpr переменную для строки? В таком случае можно использовать non-owning std::string_view (не нужны выделения памяти):
#include <iostream>
#include <string>
int main() {
constexpr auto str = std::string_view{"Hello, world!"};
std::cout << str << '\n';
}
А если нужны какие-то сложные манипуляции над строкой (раз в вопросе использована отдельная функция для этого)? Тогда можно использовать следующую технику (основано на идее из C++ Weekly - Ep 313):
#include <algorithm>
#include <array>
#include <concepts>
#include <cstddef>
#include <iterator>
#include <string>
#include <string_view>
template <typename value_type>
struct oversized_array {
std::array<value_type, std::size_t{1 << 10}> data; // large enough size constant
typename decltype(data)::size_type size;
constexpr auto begin() const noexcept {
return std::begin(data);
}
constexpr auto end() const noexcept {
using offset_type = typename std::iterator_traits<decltype(std::begin(data))>::difference_type;
return std::next(std::begin(data), static_cast<offset_type>(size));
}
};
template <std::convertible_to<std::string_view> string_type>
constexpr auto to_oversized_array(const string_type& str) {
auto array = oversized_array<typename string_type::value_type>{.size = std::size(str)};
std::copy(std::cbegin(str), std::cend(str), std::begin(array.data));
return array;
}
template <std::invocable invocable_type>
constexpr auto to_right_sized_array(invocable_type invocable) {
constexpr auto oversized_array = to_oversized_array(invocable());
using value_type = typename std::decay_t<decltype(oversized_array.data)>::value_type;
std::array<value_type, oversized_array.size> array;
std::copy(std::cbegin(oversized_array), std::cend(oversized_array), std::begin(array));
return array;
}
template <auto data>
inline constexpr auto& make_static = data;
template <std::invocable invocable_type>
constexpr auto to_string_view(invocable_type invocable) {
const auto& static_data = make_static<to_right_sized_array(invocable)>;
using value_type = typename std::decay_t<decltype(static_data)>::value_type;
return std::basic_string_view<value_type>(std::cbegin(static_data), std::cend(static_data));
}
constexpr auto get_str() {
auto complex_operations = []() constexpr {
auto str = std::string{"Hello, world!"};
str.replace(0, 1, "h");
return str;
};
return to_string_view(complex_operations);
}
int main() {
constexpr auto str = get_str();
return str.size();
}
Память, выделенная в куче во время компиляции, должна быть освобождена во время компиляции (ее нельзя пронести в рантайм), иначе выделение памяти не считается константным выражением.
Поскольку main() выполняется в рантайме, переменная str тоже должна существовать в рантайме, т. е. память должна быть освобождена не во время компиляции, а позднее, в рантайме.
Поэтому, например, вот так можно:
constexpr std::size_t foo()
{
return std::string("Hello, world!").size();
}
constexpr std::size_t x = foo();
Тут строка сразу уничтожается, и не доживает до рантайма.
Чтобы починить код в вопросе, символы должны быть не в куче, а в std::array или его аналоге.
Интересно, почему тут вообще память выделяется в куче, если строка достаточно короткая для SSO. Видимо оно отключается в constexpr выражениях, потому что хитрые трюки с union-ами/reinterpret_cast-ами не работают во время компиляции...