libstdc++: Implement C++26 type checking for std::format args [PR115776]

Implement the changes from P2757R3, which enhance the parse context to
be able to do type checking on format arguments, and to use that to
ensure that args used for width and precisions are integral types.

libstdc++-v3/ChangeLog:

	PR libstdc++/115776
	* include/bits/version.def (format): Update for C++26.
	* include/bits/version.h: Regenerate.
	* include/std/format (basic_format_parse_context): Remove
	default argument from constructor and split into two
	constructors. Make the constructor taking size_t private for
	C++26 and later.
	(basic_format_parse_context::check_dynamic_spec): New member
	function template.
	(basic_format_parse_context::check_dynamic_spec_integral): New
	member function.
	(basic_format_parse_context::check_dynamic_spec_string):
	Likewise.
	(__format::_Spec::_S_parse_width_or_precision): Use
	check_dynamic_spec_integral.
	(__format::__to_arg_t_enum): New helper function.
	(basic_format_arg):  Declare __to_arg_t_enum as friend.
	(__format::_Scanner): Define and use a derived parse context
	type.
	(__format::_Checking_scanner): Make arg types available to parse
	context.
	* testsuite/std/format/functions/format.cc: Check for new values
	of __cpp_lib_format macro.
	* testsuite/std/format/parse_ctx.cc: Check all members of
	basic_format_parse_context.
	* testsuite/std/format/parse_ctx_neg.cc: New test.
	* testsuite/std/format/string.cc: Add more checks for dynamic
	width and precision args.
This commit is contained in:
Jonathan Wakely
2024-07-09 12:12:56 +01:00
committed by Jonathan Wakely
parent 72cd15b20a
commit 3836df7e89
7 changed files with 347 additions and 13 deletions

View File

@@ -1165,11 +1165,11 @@ ftms = {
// 202305 P2757R3 Type checking format args
// 202306 P2637R3 Member visit
// 202311 P2918R2 Runtime format strings II
// values = {
// v = 202305;
// cxxmin = 26;
// hosted = yes;
// };
values = {
v = 202305;
cxxmin = 26;
hosted = yes;
};
// 201907 Text Formatting, Integration of chrono, printf corner cases.
// 202106 std::format improvements.
// 202110 Fixing locale handling in chrono formatters, generator-like types.

View File

@@ -1304,7 +1304,12 @@
#undef __glibcxx_want_barrier
#if !defined(__cpp_lib_format)
# if (__cplusplus >= 202002L) && _GLIBCXX_HOSTED
# if (__cplusplus > 202302L) && _GLIBCXX_HOSTED
# define __glibcxx_format 202305L
# if defined(__glibcxx_want_all) || defined(__glibcxx_want_format)
# define __cpp_lib_format 202305L
# endif
# elif (__cplusplus >= 202002L) && _GLIBCXX_HOSTED
# define __glibcxx_format 202304L
# if defined(__glibcxx_want_all) || defined(__glibcxx_want_format)
# define __cpp_lib_format 202304L

View File

@@ -222,6 +222,9 @@ namespace __format
inline void
__failed_to_parse_format_spec()
{ __throw_format_error("format error: failed to parse format-spec"); }
template<typename _CharT> class _Scanner;
} // namespace __format
/// @endcond
@@ -241,9 +244,8 @@ namespace __format
using iterator = const_iterator;
constexpr explicit
basic_format_parse_context(basic_string_view<_CharT> __fmt,
size_t __num_args = 0) noexcept
: _M_begin(__fmt.begin()), _M_end(__fmt.end()), _M_num_args(__num_args)
basic_format_parse_context(basic_string_view<_CharT> __fmt) noexcept
: _M_begin(__fmt.begin()), _M_end(__fmt.end())
{ }
basic_format_parse_context(const basic_format_parse_context&) = delete;
@@ -283,13 +285,78 @@ namespace __format
__format::__invalid_arg_id_in_format_string();
}
#if __cpp_lib_format >= 202305L
template<typename... _Ts>
constexpr void
check_dynamic_spec(size_t __id) noexcept;
constexpr void
check_dynamic_spec_integral(size_t __id) noexcept
{
check_dynamic_spec<int, unsigned, long long, unsigned long long>(__id);
}
constexpr void
check_dynamic_spec_string(size_t __id) noexcept
{
check_dynamic_spec<const char_type*, basic_string_view<_CharT>>(__id);
}
private:
// Check the Mandates: condition for check_dynamic_spec<Ts...>(n)
template<typename... _Ts>
static consteval bool
__check_dynamic_spec_types()
{
if constexpr (sizeof...(_Ts))
{
int __counts[] = {
(is_same_v<bool, _Ts> + ...),
(is_same_v<_CharT, _Ts> + ...),
(is_same_v<int, _Ts> + ...),
(is_same_v<unsigned, _Ts> + ...),
(is_same_v<long long, _Ts> + ...),
(is_same_v<unsigned long long, _Ts> + ...),
(is_same_v<float, _Ts> + ...),
(is_same_v<double, _Ts> + ...),
(is_same_v<long double, _Ts> + ...),
(is_same_v<const _CharT*, _Ts> + ...),
(is_same_v<basic_string_view<_CharT>, _Ts> + ...),
(is_same_v<const void*, _Ts> + ...)
};
int __sum = 0;
for (int __c : __counts)
{
__sum += __c;
if (__c > 1)
__invalid_dynamic_spec("non-unique template argument type");
}
if (__sum != sizeof...(_Ts))
__invalid_dynamic_spec("disallowed template argument type");
}
return true;
}
// This must not be constexpr.
static void __invalid_dynamic_spec(const char*);
friend __format::_Scanner<_CharT>;
#endif
// This constructor should only be used by the implementation.
constexpr explicit
basic_format_parse_context(basic_string_view<_CharT> __fmt,
size_t __num_args) noexcept
: _M_begin(__fmt.begin()), _M_end(__fmt.end()), _M_num_args(__num_args)
{ }
private:
iterator _M_begin;
iterator _M_end;
enum _Indexing { _Unknown, _Manual, _Auto };
_Indexing _M_indexing = _Unknown;
size_t _M_next_arg_id = 0;
size_t _M_num_args;
size_t _M_num_args = 0;
};
/// @cond undocumented
@@ -549,6 +616,9 @@ namespace __format
__pc.check_arg_id(__v);
__val = __v;
}
#if __cpp_lib_format >= 202305L
__pc.check_dynamic_spec_integral(__val);
#endif
++__first; // past the '}'
}
return __first;
@@ -3205,6 +3275,10 @@ namespace __format
template<typename _Context, typename... _Args>
class _Arg_store;
template<typename _Ch, typename _Tp>
consteval _Arg_t
__to_arg_t_enum() noexcept;
} // namespace __format
/// @endcond
@@ -3486,6 +3560,10 @@ namespace __format
friend decltype(auto)
visit_format_arg(_Visitor&& __vis, basic_format_arg<_Ctx>);
template<typename _Ch, typename _Tp>
friend consteval __format::_Arg_t
__format::__to_arg_t_enum() noexcept;
template<typename _Visitor>
decltype(auto)
_M_visit(_Visitor&& __vis, __format::_Arg_t __type)
@@ -3899,7 +3977,11 @@ namespace __format
{
using iterator = typename basic_format_parse_context<_CharT>::iterator;
basic_format_parse_context<_CharT> _M_pc;
struct _Parse_context : basic_format_parse_context<_CharT>
{
using basic_format_parse_context<_CharT>::basic_format_parse_context;
const _Arg_t* _M_types = nullptr;
} _M_pc;
constexpr explicit
_Scanner(basic_string_view<_CharT> __str, size_t __nargs = -1)
@@ -4059,6 +4141,16 @@ namespace __format
}
};
template<typename _CharT, typename _Tp>
consteval _Arg_t
__to_arg_t_enum() noexcept
{
using _Context = __format::__format_context<_CharT>;
using _Fmt_arg = basic_format_arg<_Context>;
using _NormalizedTp = typename _Fmt_arg::template _Normalize<_Tp>;
return _Fmt_arg::template _S_to_enum<_NormalizedTp>();
}
// Validate a format string for Args.
template<typename _CharT, typename... _Args>
class _Checking_scanner : public _Scanner<_CharT>
@@ -4068,10 +4160,14 @@ namespace __format
"std::formatter must be specialized for each type being formatted");
public:
constexpr
consteval
_Checking_scanner(basic_string_view<_CharT> __str)
: _Scanner<_CharT>(__str, sizeof...(_Args))
{ }
{
#if __cpp_lib_format >= 202305L
this->_M_pc._M_types = _M_types.data();
#endif
}
private:
constexpr void
@@ -4102,6 +4198,11 @@ namespace __format
else
__builtin_unreachable();
}
#if __cpp_lib_format >= 202305L
array<_Arg_t, sizeof...(_Args)>
_M_types{ { __format::__to_arg_t_enum<_CharT, _Args>()... } };
#endif
};
template<typename _Out, typename _CharT, typename _Context>
@@ -4200,6 +4301,33 @@ namespace __format
} // namespace __format
/// @endcond
#if __cpp_lib_format >= 202305L
template<typename _CharT>
template<typename... _Ts>
constexpr void
basic_format_parse_context<_CharT>::check_dynamic_spec(size_t __id) noexcept
{
constexpr bool __ok = __check_dynamic_spec_types<_Ts...>();
if consteval {
if (__id >= _M_num_args)
__format::__invalid_arg_id_in_format_string();
if constexpr (sizeof...(_Ts) != 0)
{
using _Parse_context = __format::_Scanner<_CharT>::_Parse_context;
auto __arg = static_cast<_Parse_context*>(this)->_M_types[__id];
__format::_Arg_t __types[] = {
__format::__to_arg_t_enum<_CharT, _Ts>()...
};
for (auto __t : __types)
if (__arg == __t)
return;
}
__invalid_dynamic_spec("arg(id) type does not match");
}
}
#endif
template<typename _CharT, typename... _Args>
template<typename _Tp>
requires convertible_to<const _Tp&, basic_string_view<_CharT>>

View File

@@ -8,6 +8,8 @@
# error "Feature test macro for std::format is missing in <format>"
#elif __cpp_lib_format < 202110L
# error "Feature test macro for std::format has wrong value in <format>"
#elif __cplusplus > 202302L && __cpp_lib_format < 202305L
# error "Feature test macro for std::format has wrong value in <format>"
#endif
#ifndef __cpp_lib_format_uchar
@@ -22,6 +24,8 @@
# error "Feature test macro for std::format is missing in <version>"
#elif __cpp_lib_format < 202110L
# error "Feature test macro for std::format has wrong value in <version>"
#elif __cplusplus > 202302L && __cpp_lib_format < 202305L
# error "Feature test macro for std::format has wrong value in <version>"
#endif
#ifndef __cpp_lib_format_uchar

View File

@@ -3,6 +3,91 @@
#include <format>
#include <testsuite_hooks.h>
static_assert(std::is_constructible_v<std::format_parse_context,
std::string_view>);
static_assert(std::is_constructible_v<std::wformat_parse_context,
std::wstring_view>);
#if __cpp_lib_format < 202305
constexpr bool construct_with_num_args = true;
#else
constexpr bool construct_with_num_args = false;
#endif
static_assert(std::is_constructible_v<std::format_parse_context,
std::string_view, std::size_t>
== construct_with_num_args);
static_assert(std::is_constructible_v<std::wformat_parse_context,
std::wstring_view, std::size_t>
== construct_with_num_args);
static_assert( ! std::is_constructible_v<std::format_parse_context,
std::wstring_view>);
static_assert( ! std::is_constructible_v<std::wformat_parse_context,
std::string_view>);
static_assert( ! std::is_convertible_v<std::string_view,
std::format_parse_context> );
static_assert( ! std::is_convertible_v<std::wstring_view,
std::wformat_parse_context> );
static_assert( ! std::is_default_constructible_v<std::format_parse_context> );
static_assert( ! std::is_copy_constructible_v<std::format_parse_context> );
static_assert( ! std::is_move_constructible_v<std::format_parse_context> );
static_assert( ! std::is_copy_assignable_v<std::format_parse_context> );
static_assert( ! std::is_move_assignable_v<std::format_parse_context> );
// This concept is satisfied if the next_arg_id() call is a constant expression
template<typename Ch, typename PC = std::basic_format_parse_context<Ch>>
concept arg_id_available = requires {
typename std::integral_constant<std::size_t,
PC({}).next_arg_id()>::type;
};
void
test_members()
{
std::string_view s = "spec string";
std::format_parse_context pc(s);
VERIFY( pc.begin() == s.begin() );
VERIFY( pc.end() == s.end() );
pc.advance_to(s.begin() + 5);
VERIFY( pc.begin() == s.begin() + 5 );
// Runtime calls to these do not check for the correct number of args.
VERIFY( pc.next_arg_id() == 0 );
VERIFY( pc.next_arg_id() == 1 );
VERIFY( pc.next_arg_id() == 2 );
try
{
// Cannot mix manual and automatic indexing.
pc.check_arg_id(0);
VERIFY( false );
}
catch (const std::format_error&)
{
}
// But they do check during constant evaluation:
VERIFY( ! arg_id_available<char> );
VERIFY( ! arg_id_available<wchar_t> );
std::format_parse_context pc2("");
pc2.check_arg_id(2);
pc2.check_arg_id(1);
pc2.check_arg_id(3);
try
{
// Cannot mix manual and automatic indexing.
(void) pc2.next_arg_id();
VERIFY( false );
}
catch (const std::format_error&)
{
}
}
template<typename T, bool auto_indexing = true>
bool
is_std_format_spec_for(std::string_view spec)
@@ -357,6 +442,65 @@ test_custom()
VERIFY( ! is_std_format_spec_for<S>("") );
}
#if __cpp_lib_format >= 202305
struct X { };
template<>
struct std::formatter<X, char>
{
constexpr std::format_parse_context::iterator
parse(std::format_parse_context& pc)
{
std::string_view spec(pc.begin(), pc.end());
auto p = spec.find('}');
if (p != std::string_view::npos)
spec = spec.substr(0, p); // truncate to closing brace
if (spec == "int")
{
pc.check_dynamic_spec_integral(pc.next_arg_id());
integer = true;
}
else if (spec == "str")
{
pc.check_dynamic_spec_string(pc.next_arg_id());
integer = false;
}
else
throw std::format_error("invalid format-spec");
return pc.begin() + spec.size();
}
std::format_context::iterator
format(X, std::format_context& c) const
{
std::visit_format_arg([this]<typename T>(T) {
if (is_integral_v<T> != this->integer)
throw std::format_error("invalid argument type");
}, c.arg(1));
return c.out();
}
private:
bool integer = false;
};
#endif
void
test_dynamic_type_check()
{
#if __cpp_lib_format >= 202305
std::format_parse_context pc("{1}.{2}");
// None of these calls should do anything at runtime, only during consteval:
pc.check_dynamic_spec<>(0);
pc.check_dynamic_spec<int, const char*>(0);
pc.check_dynamic_spec_integral(0);
pc.check_dynamic_spec_string(0);
(void) std::format("{:int}", X{}, 42L);
(void) std::format("{:str}", X{}, "H2G2");
#endif
}
int main()
{
test_char();
@@ -366,4 +510,5 @@ int main()
test_string();
test_pointer();
test_custom();
test_dynamic_type_check();
}

View File

@@ -0,0 +1,39 @@
// { dg-do compile { target c++26 } }
#include <format>
void
test_invalid()
{
std::format_parse_context pc("");
// These types are all valid:
pc.check_dynamic_spec<bool, char, int, unsigned, long long,
unsigned long long, float, double, long double,
const char*, std::string_view, const void*>(0);
// For some reason, an empty pack of types is valid:
pc.check_dynamic_spec<>(0);
pc.check_dynamic_spec<void>(0); // { dg-error "here" }
// const void* is allowed, but void* is not
pc.check_dynamic_spec<void*>(0); // { dg-error "here" }
// int and long long are allowed, but long is not
pc.check_dynamic_spec<long>(0); // { dg-error "here" }
// char_type is allowed, but other character types are not
pc.check_dynamic_spec<wchar_t>(0); // { dg-error "here" }
pc.check_dynamic_spec<char8_t>(0); // { dg-error "here" }
// std::string_view is allowed, but std::string is not
pc.check_dynamic_spec<std::string>(0); // { dg-error "here" }
pc.check_dynamic_spec<int, bool, int>(0); // { dg-error "here" }
std::wformat_parse_context wpc(L"");
wpc.check_dynamic_spec<bool, wchar_t, int, unsigned, long long,
unsigned long long, float, double, long double,
const wchar_t*, std::wstring_view, const void*>(0);
wpc.check_dynamic_spec<char>(0); // { dg-error "here" }
wpc.check_dynamic_spec<char16_t>(0); // { dg-error "here" }
wpc.check_dynamic_spec<char32_t>(0); // { dg-error "here" }
}
// Each failure above will point to a call to this non-constexpr function:
// { dg-error "__invalid_dynamic_spec" "" { target *-*-* } 0 }

View File

@@ -109,9 +109,16 @@ test_format_spec()
VERIFY( ! is_format_string_for("{:#?}", "str") );
VERIFY( ! is_format_string_for("{:#?}", 'c') );
// The 0 option is not valid for charT and bool.
VERIFY( ! is_format_string_for("{:0c}", 'c') );
VERIFY( ! is_format_string_for("{:0s}", true) );
// Dynamic width arg must be an integer type.
VERIFY( ! is_format_string_for("{:{}d}", 1, 1.5) );
VERIFY( ! is_format_string_for("{:{}d}", 1, true) );
VERIFY( ! is_format_string_for("{:{}d}", 1, "str") );
VERIFY( ! is_format_string_for("{:{}d}", 1, nullptr) );
// Precision only valid for string and floating-point types.
VERIFY( ! is_format_string_for("{:.3d}", 1) );
VERIFY( ! is_format_string_for("{:3.3d}", 1) );
@@ -119,6 +126,12 @@ test_format_spec()
VERIFY( ! is_format_string_for("{:3.3s}", 'c') );
VERIFY( ! is_format_string_for("{:3.3p}", nullptr) );
// Dynamic precision arg must be an integer type.
VERIFY( ! is_format_string_for("{:.{}f}", 1.0, 1.5) );
VERIFY( ! is_format_string_for("{:.{}f}", 1.0, true) );
VERIFY( ! is_format_string_for("{:.{}f}", 1.0, "str") );
VERIFY( ! is_format_string_for("{:.{}f}", 1.0, nullptr) );
// Invalid presentation types for integers.
VERIFY( ! is_format_string_for("{:f}", 1) );
VERIFY( ! is_format_string_for("{:s}", 1) );