Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Report error on duplicate named arg names #4367

Merged
merged 10 commits into from
Mar 2, 2025

Conversation

dinomight
Copy link
Contributor

Compile error will happen if static named args are used and enabled. Runtime exception will be thrown in other cases.
#4282

@dinomight
Copy link
Contributor Author

Wanted to mention that this does not handle the case of using FMT_COMPILE strings. That infrastructure is a bit different and doesn't utilize either of the functions I've updated here. It should be doable for FMT_COMPILE string, but might require additional memory to do the duplicate name check. I'm not sure what the preferred technique is there. Should I file a separate ticket to track?

Copy link
Contributor

@vitaut vitaut left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR!

Comment on lines 1074 to 1079
for (auto i = 0; i < named_arg_index; ++i) {
if (basic_string_view<Char>(named_args[i].name) ==
basic_string_view<Char>(arg.name)) {
report_error("duplicate named args found");
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move it to a separate function in detail and reuse here and below.

@@ -1071,6 +1071,12 @@ void init_named_arg(named_arg_info<Char>*, int& arg_index, int&, const T&) {
template <typename Char, typename T, FMT_ENABLE_IF(is_named_arg<T>::value)>
void init_named_arg(named_arg_info<Char>* named_args, int& arg_index,
int& named_arg_index, const T& arg) {
for (auto i = 0; i < named_arg_index; ++i) {
if (basic_string_view<Char>(named_args[i].name) ==
basic_string_view<Char>(arg.name)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's better to move this out of the loop to avoid recomputing length.

Comment on lines 212 to 219
expect_compile(format-string-duplicate-name-error "
#if defined(FMT_HAS_CONSTEVAL) && FMT_USE_NONTYPE_TEMPLATE_ARGS
using namespace fmt::literals;
fmt::print(\"{bar}\", \"bar\"_a=42, \"bar\"_a=43);
#else
#error
#endif
" ERROR)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests are expensive and we don't need to use them here, please remove.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are the situations where you'd want to add these? I figured it was important to verify that compile fails in this situation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is basically an integration test that checks that the internal checking machinery translates into actual compiler errors. It shouldn't be used for unit testing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I'm a little confused because that's what this is doing. I wrote a unit test for the runtime exception version. I figured since there's also a compile time failure, a test would be required to make sure it functions, especially since the two routes are different in the code.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could unit test that exception is thrown but I think it's fine as is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did add a test to verify dynamic named args cause exceptions at runtime. In order to compile the above lines and verify it throws would require I only enable the test when we're not using non-type template parameters. In that situation, it just devolves to the dynamic named args and we don't test the static, compile-time check machinery, right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compile-time check machinery just translates exceptions to compile-time errors and it's (roughly) the same for all errors so it doesn't need to be tested multiple times. It's sufficient to test the exception path.

@vitaut
Copy link
Contributor

vitaut commented Feb 28, 2025

Should I file a separate ticket to track?

Just add a comment in #4124 since it's part of general named argument support in format string compilation.

@dinomight
Copy link
Contributor Author

dinomight commented Mar 1, 2025

I'm not sure why the refactoring made GCC13 unhappy, but it's complaining about possibly uninitialized values being referenced. I can initialize those values in the field and make GCC13 happy, but then GCC10 with C++11 breaks because of some initializer list-style initializations. I'm going to try and add a constructor to make everybody happy and you can let me know if there is a better way.

For reference, this was the warning that was treated as an error:

In function ‘void fmt::v11::detail::init_named_arg(named_arg_info<Char>*, int&, int&, const T&) [with Char = char; T = static_named_arg<const char (&)[5], char, 6, fixed_string<char, 6>{"first"}>; typename std::enable_if<is_named_arg<T>::value, int>::type <anonymous> = 0]’,
    inlined from ‘constexpr fmt::v11::detail::named_arg_store<Context, NUM_ARGS, NUM_NAMED_ARGS, DESC>::named_arg_store(T& ...) [with T = {fmt::v11::detail::static_named_arg<const char (&)[5], char, 6, fmt::v11::detail::fixed_string<char, 6>{"first"}>, fmt::v11::detail::static_named_arg<const char (&)[4], char, 7, fmt::v11::detail::fixed_string<char, 7>{"second"}>, fmt::v11::detail::static_named_arg<int, char, 6, fmt::v11::detail::fixed_string<char, 6>{"third"}>}; Context = fmt::v11::context; int NUM_ARGS = 3; int NUM_NAMED_ARGS = 3; long long unsigned int DESC = 460]’ at /home/runner/work/fmt/fmt/include/fmt/base.h:2336:5,
    inlined from ‘std::string fmt::v11::format(format_string<T ...>, T&& ...) [with T = {detail::static_named_arg<const char (&)[5], char, 6, detail::fixed_string<char, 6>{"first"}>, detail::static_named_arg<const char (&)[4], char, 7, detail::fixed_string<char, 7>{"second"}>, detail::static_named_arg<int, char, 6, detail::fixed_string<char, 6>{"third"}>}]’ at /home/runner/work/fmt/fmt/include/fmt/format.h:4200:49,
    inlined from ‘virtual void format_test_named_arg_udl_Test::TestBody()’ at /home/runner/work/fmt/fmt/test/format-test.cc:2035:62:
/home/runner/work/fmt/fmt/include/fmt/base.h:1087:22: error: ‘<anonymous>’ may be used uninitialized [-Werror=maybe-uninitialized]
 1087 |   check_for_duplicate(named_args, named_arg_index, arg.name);
      |   ~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/home/runner/work/fmt/fmt/include/fmt/base.h: In member function ‘virtual void format_test_named_arg_udl_Test::TestBody()’:
/home/runner/work/fmt/fmt/include/fmt/base.h:1068:20: note: by argument 1 of type ‘const fmt::v11::detail::named_arg_info<char>*’ to ‘constexpr void fmt::v11::detail::check_for_duplicate(const named_arg_info<Char>*, int, const Char*) [with Char = char]’ declared here
 1068 | FMT_CONSTEXPR void check_for_duplicate(
      |                    ^~~~~~~~~~~~~~~~~~~
/home/runner/work/fmt/fmt/include/fmt/format.h:4200:17: note: ‘<anonymous>’ declared here
 4200 |   return vformat(fmt.str, vargs<T...>{{args...}});
      |          ~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Comment on lines 1063 to 1065
FMT_CONSTEXPR named_arg_info() : name(nullptr), id(0) {}
FMT_CONSTEXPR named_arg_info(const Char* a_name, const int an_id)
: name(a_name), id(an_id) {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initialization is done in init*named_arg, we shouldn't duplicate it here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to try your idea of only calling if the index is greater than zero, but if that doesn't work, I need the default construct and if I have the default construct, GCC10 will need the other one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both functions have been removed. The if check appeased GCC13, it seems.

@@ -1084,6 +1101,7 @@ template <typename T, typename Char,
FMT_ENABLE_IF(is_static_named_arg<T>::value)>
FMT_CONSTEXPR void init_static_named_arg(named_arg_info<Char>* named_args,
int& arg_index, int& named_arg_index) {
check_for_duplicate(named_args, named_arg_index, T::name);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if calling check_for_duplicate only if named_arg_index is nonzero fixes the uninitialized warning.

Comment on lines 1073 to 1074
const Char* const arg_name) {
const basic_string_view<Char> arg_name_view(arg_name);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can simplify this by passing arg_name as a string_view.

const Char* name;
int id;
};

template <typename Char>
FMT_CONSTEXPR void check_for_duplicate(
const named_arg_info<Char>* const named_args, const int named_arg_index,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not overuse const - it just clutters the code without bringing much benefit when the scope is small.

Copy link
Contributor Author

@dinomight dinomight Mar 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just want my immutable by default like rust 😄 . This is also an area I disagree with the CPP core guidelines. I'll begrudgingly update this, though.

@vitaut vitaut merged commit 864bdf9 into fmtlib:master Mar 2, 2025
45 checks passed
@vitaut
Copy link
Contributor

vitaut commented Mar 2, 2025

Merged, thanks. One drawback of this is quadratic complexity but this can be addressed later.

@dinomight dinomight deleted the duplicate-named-args branch March 2, 2025 16:04
Comment on lines +1068 to +1071
FMT_CONSTEXPR void check_for_duplicate(named_arg_info<Char>* named_args,
int named_arg_index,
basic_string_view<Char> arg_name) {
if (named_arg_index <= 0) return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As it turned out the actual change that worked around the gcc bug was removing const in named_args, not adding the check =)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, that's kind of funny. Which const was it, cause I had a lot 😄 Makes me curious why the const additions made it seem like it might not be initialized to GCC. Maybe it just generated some weird assumptions in the compiler checks as part of the inlining process?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const named_arg_info<Char>*, looks like https://gcc.gnu.org/bugzilla/show_bug.cgi?id=100417

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants