From 4b5aa70e7331e9553efc1144da7fc746c04ab340 Mon Sep 17 00:00:00 2001 From: charlie Date: Tue, 4 Aug 2026 18:56:35 -0500 Subject: [PATCH] Add operator<< to the enum macro family Generate std::ostream& operator<<(std::ostream&, name) alongside to_string for every MIGRAPHX_ENUM variant, so a named enum streams as its enumerator name instead of its underlying integer. The overload is an exact match, so it wins over the conversion to the underlying type for unscoped enums, and it is emitted after to_string so the nested (friend) variant reaches it by ADL. The namespace-scope variants now take their linkage from MIGRAPHX_DETAIL_ENUM_INLINE ([[maybe_unused]] inline). These enums belong in an anonymous namespace in a .cpp, which gives the generated helpers internal linkage, so any the file never calls would otherwise trip -Wunused-function. Co-authored-by: Cursor --- src/include/migraphx/enum.hpp | 149 +++++++++++++++++++--------------- test/enum.cpp | 65 +++++++++++++++ 2 files changed, 147 insertions(+), 67 deletions(-) diff --git a/src/include/migraphx/enum.hpp b/src/include/migraphx/enum.hpp index a2a59e7a42d..53c70ee941c 100644 --- a/src/include/migraphx/enum.hpp +++ b/src/include/migraphx/enum.hpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -45,11 +46,9 @@ inline namespace MIGRAPHX_INLINE_NS { namespace detail { -// enum_capture and enum_capturer implement the value capturing used by MIGRAPHX_ENUM. Each -// enumerator `e` in the list is rewritten to `enum_capturer{}->*e`. Since operator->* binds -// more tightly than assignment, `enum_capturer{}->*e = 42` parses as -// `(enum_capturer{}->*e) = 42`: operator->* captures the real value of the enumerator (which -// already accounts for the `= 42`), and operator= simply swallows the initializer. +// Value capturing for MIGRAPHX_ENUM: each enumerator `e` becomes `enum_capturer{}->*e`. operator->* +// binds tighter than assignment, so `enum_capturer{}->*e = 42` captures e's real value (which +// already accounts for the `= 42`) and operator= swallows the initializer. template struct enum_capture { @@ -73,16 +72,15 @@ struct enum_capturer } }; -// Wraps an enumerator as a type so that get_type_name renders it as its name: the compiler spells -// a named enumerator as its own identifier, e.g. get_type_name>() is -// "...enum_value". +// Wraps an enumerator as a type so get_type_name spells it by name, e.g. +// get_type_name>() is "...enum_value". template struct enum_value { }; -// Recovers the enumerator name from that type name, e.g. "green" from "...enum_value" -// or "on" from "...enum_value" (scoped and nested enumerators are qualified). +// Recovers the enumerator name from that type name, stripping the qualification that scoped and +// nested enumerators carry: "...enum_value" gives "on". template std::string enum_value_name() { @@ -102,9 +100,9 @@ auto enum_value_names() }); } -// Maps an enumerator value to its name. The value set comes from migraphx_enum_entries, but the -// names are derived from the values through get_type_name rather than from any stored strings. The -// value -> name table is built once per enum so lookups are O(1). +// Maps an enumerator value to its name. Values come from migraphx_enum_entries, names from +// get_type_name rather than any stored strings. The table is built once per enum, so lookups are +// O(1). template std::string enum_to_string(Enum value) { @@ -128,8 +126,7 @@ std::string enum_to_string(Enum value) } // namespace detail -// Detects enums declared with the MIGRAPHX_ENUM family: those provide a migraphx_enum_entries hook -// and therefore support to_string and migraphx::from_string. +// True for enums from the MIGRAPHX_ENUM family, which provide the migraphx_enum_entries hook. template struct is_named_enum : std::false_type { @@ -148,8 +145,8 @@ auto enum_entries() return migraphx_enum_entries(Enum{}); } -// Converts the name of an enumerator back into its value, throwing when the name is unknown. The -// name -> value table is built once per enum from migraphx_enum_entries so lookups are O(1). +// Converts an enumerator name back into its value, throwing when the name is unknown. The table is +// built once per enum, so lookups are O(1). template {})> Enum from_string(const std::string& name) { @@ -171,40 +168,51 @@ Enum from_string(const std::string& name) } // namespace MIGRAPHX_INLINE_NS } // namespace migraphx -// Rewrites a single enumerator `x` (which may include an `= value`) so that operator->* captures -// its value. See the note on enum_capture above for why operator->* is used here. +// Rewrites one enumerator `x` (possibly `x = value`) so operator->* captures its value; see the +// note on enum_capture above. #define MIGRAPHX_DETAIL_ENUM_CAPTURE(x) (migraphx::detail::enum_capturer{}->*x) -// The scoped-enum variant qualifies the enumerator with `enum_scope`, a local alias for the enum -// type that MIGRAPHX_ENUM_CLASS declares in the entries function (scoped enumerators are not -// visible unqualified). +// Scoped variant: qualifies with `enum_scope`, the alias the entries function declares, since +// scoped enumerators are not visible unqualified. #define MIGRAPHX_DETAIL_ENUM_CLASS_CAPTURE(x) (migraphx::detail::enum_capturer{}->*enum_scope::x) -// Generates the ADL hooks (migraphx_enum_entries + to_string) shared by the MIGRAPHX_ENUM family. -// `linkage` is `inline` for a namespace-scope enum or `friend` for a class-scope (nested) enum; -// `capture` is the per-enumerator capture macro; `prologue` runs before the capture list and is -// used by the scoped variants to declare the enum_scope alias. migraphx_enum_entries returns just -// the array of enumerator values; to_string recovers the names from those values via get_type_name. +// Linkage for the namespace-scope variants. These enums belong in an anonymous namespace in a .cpp, +// which gives the helpers internal linkage, so any the file never calls would trip +// -Wunused-function. The nested variants generate hidden friends and need no marking. +#define MIGRAPHX_DETAIL_ENUM_INLINE [[maybe_unused]] inline + +// Generates the ADL hooks (migraphx_enum_entries, to_string, operator<<) shared by the family. +// `linkage` is MIGRAPHX_DETAIL_ENUM_INLINE at namespace scope or `friend` for a nested enum; +// `capture` is the per-enumerator capture macro; `prologue` declares the enum_scope alias for the +// scoped variants. operator<< comes after to_string so the friend variant reaches it by ADL. #ifdef CPPCHECK -// cppcheck's preprocessor cannot expand the recursive MIGRAPHX_PP_TRANSFORM_ARGS, so generate the -// hooks without it; the captured values are irrelevant to static analysis. -#define MIGRAPHX_DETAIL_ENUM_HELPERS(linkage, name, capture, prologue, ...) \ - linkage constexpr auto migraphx_enum_entries(name) \ - { \ - return migraphx::make_array(name{}); \ - } \ - linkage std::string to_string(name value) { return migraphx::detail::enum_to_string(value); } +// cppcheck cannot expand the recursive MIGRAPHX_PP_TRANSFORM_ARGS; the captured values do not +// matter to static analysis. +#define MIGRAPHX_DETAIL_ENUM_HELPERS(linkage, name, capture, prologue, ...) \ + linkage constexpr auto migraphx_enum_entries(name) \ + { \ + return migraphx::make_array(name{}); \ + } \ + linkage std::string to_string(name value) { return migraphx::detail::enum_to_string(value); } \ + linkage std::ostream& operator<<(std::ostream& os, name value) \ + { \ + return os << to_string(value); \ + } #else -#define MIGRAPHX_DETAIL_ENUM_HELPERS(linkage, name, capture, prologue, ...) \ - linkage constexpr auto migraphx_enum_entries(name) \ - { \ - prologue return migraphx::make_array( \ - MIGRAPHX_PP_TRANSFORM_ARGS(capture, __VA_ARGS__)); \ - } \ - linkage std::string to_string(name value) { return migraphx::detail::enum_to_string(value); } +#define MIGRAPHX_DETAIL_ENUM_HELPERS(linkage, name, capture, prologue, ...) \ + linkage constexpr auto migraphx_enum_entries(name) \ + { \ + prologue return migraphx::make_array( \ + MIGRAPHX_PP_TRANSFORM_ARGS(capture, __VA_ARGS__)); \ + } \ + linkage std::string to_string(name value) { return migraphx::detail::enum_to_string(value); } \ + linkage std::ostream& operator<<(std::ostream& os, name value) \ + { \ + return os << to_string(value); \ + } #endif -// Declares an unscoped enum and generates `to_string` and `migraphx::from_string` helpers that +// Declares an unscoped enum plus `to_string`, `migraphx::from_string`, and `operator<<`, which // convert its enumerators to and from their names. Use it at namespace scope: // // MIGRAPHX_ENUM(color, @@ -214,18 +222,21 @@ Enum from_string(const std::string& name) // // std::string s = to_string(green); // "green" // color c = migraphx::from_string("blue"); // blue +// std::cout << green; // prints "green" // -// Enumerators may take explicit values, and up to 63 are supported. When used in a .cpp instead of -// a header, place it in an anonymous namespace so the generated helpers get internal linkage. +// operator<< is an exact match, so it beats the conversion to the underlying type: the enum streams +// as its name, not its number. Up to 63 enumerators, which may take explicit values. In a .cpp, +// declare it in an anonymous namespace so the helpers get internal linkage. // NOLINTNEXTLINE(cppcoreguidelines-macro-usage) -#define MIGRAPHX_ENUM(name, ...) \ - enum name \ - { \ - __VA_ARGS__ \ - }; \ - MIGRAPHX_DETAIL_ENUM_HELPERS(inline, name, MIGRAPHX_DETAIL_ENUM_CAPTURE, , __VA_ARGS__) +#define MIGRAPHX_ENUM(name, ...) \ + enum name \ + { \ + __VA_ARGS__ \ + }; \ + MIGRAPHX_DETAIL_ENUM_HELPERS( \ + MIGRAPHX_DETAIL_ENUM_INLINE, name, MIGRAPHX_DETAIL_ENUM_CAPTURE, , __VA_ARGS__) -// Like MIGRAPHX_ENUM, but declares a scoped enum (enum class): +// Like MIGRAPHX_ENUM, but a scoped enum (enum class): // // MIGRAPHX_ENUM_CLASS(color, // red, @@ -234,21 +245,24 @@ Enum from_string(const std::string& name) // // std::string s = to_string(color::green); // "green" // color c = migraphx::from_string("blue"); // color::blue +// std::cout << color::green; // prints "green" // -// An explicit enumerator value must be self-contained: it cannot reference another enumerator, -// which is not visible unqualified in a scoped enum. +// An explicit value must be self-contained: it cannot reference another enumerator, which is not +// visible unqualified in a scoped enum. // NOLINTNEXTLINE(cppcoreguidelines-macro-usage) -#define MIGRAPHX_ENUM_CLASS(name, ...) \ - enum class name \ - { \ - __VA_ARGS__ \ - }; \ - MIGRAPHX_DETAIL_ENUM_HELPERS( \ - inline, name, MIGRAPHX_DETAIL_ENUM_CLASS_CAPTURE, using enum_scope = name;, __VA_ARGS__) +#define MIGRAPHX_ENUM_CLASS(name, ...) \ + enum class name \ + { \ + __VA_ARGS__ \ + }; \ + MIGRAPHX_DETAIL_ENUM_HELPERS(MIGRAPHX_DETAIL_ENUM_INLINE, \ + name, \ + MIGRAPHX_DETAIL_ENUM_CLASS_CAPTURE, \ + using enum_scope = name; \ + , __VA_ARGS__) -// Like MIGRAPHX_ENUM, but for an enum declared inside a class or struct. The helpers are generated -// as hidden friends instead of free functions so that argument-dependent lookup still finds them. -// Use it inside the class/struct body: +// Like MIGRAPHX_ENUM, but for an enum inside a class or struct: the helpers become hidden friends +// rather than free functions, so ADL still finds them. Use it in the class body: // // struct widget // { @@ -257,6 +271,7 @@ Enum from_string(const std::string& name) // // std::string s = to_string(widget::on); // "on" // widget::mode m = migraphx::from_string("off"); // widget::off +// std::cout << widget::on; // prints "on" // NOLINTNEXTLINE(cppcoreguidelines-macro-usage) #define MIGRAPHX_NESTED_ENUM(name, ...) \ enum name \ @@ -265,9 +280,8 @@ Enum from_string(const std::string& name) }; \ MIGRAPHX_DETAIL_ENUM_HELPERS(friend, name, MIGRAPHX_DETAIL_ENUM_CAPTURE, , __VA_ARGS__) -// Like MIGRAPHX_NESTED_ENUM, but declares a scoped enum (enum class); it relates to -// MIGRAPHX_NESTED_ENUM as MIGRAPHX_ENUM_CLASS does to MIGRAPHX_ENUM, including the same restriction -// on explicit enumerator values. Use it inside the class/struct body: +// Like MIGRAPHX_NESTED_ENUM, but a scoped enum, with the same restriction on explicit values as +// MIGRAPHX_ENUM_CLASS. Use it in the class body: // // struct widget // { @@ -276,6 +290,7 @@ Enum from_string(const std::string& name) // // std::string s = to_string(widget::unit::cm); // "cm" // widget::unit u = migraphx::from_string("mm"); // widget::unit::mm +// std::cout << widget::unit::cm; // prints "cm" // NOLINTNEXTLINE(cppcoreguidelines-macro-usage) #define MIGRAPHX_NESTED_ENUM_CLASS(name, ...) \ enum class name \ diff --git a/test/enum.cpp b/test/enum.cpp index 14c9f313c3d..94e3aa4ff66 100644 --- a/test/enum.cpp +++ b/test/enum.cpp @@ -24,9 +24,12 @@ #include #include #include +#include #include +#include #include #include +#include #include "test.hpp" // These enums are test-local fixtures; the anonymous namespace gives the macro-generated helper @@ -40,6 +43,11 @@ MIGRAPHX_ENUM(my_enum, first, last = 10) // Single enumerator, exercises the base case of the value-capturing expansion. MIGRAPHX_ENUM(solo, only_one) +// Declared and never used at all. Guards the [[maybe_unused]] on the generated namespace-scope +// helpers: in an anonymous namespace they have internal linkage, so without it an enum whose +// helpers this file never calls would warn under -Wunused-function. +MIGRAPHX_ENUM(never_used, never_used_value) + // Expression-valued enumerators, including one that references a previous enumerator. MIGRAPHX_ENUM(flags, none = 0, bit0 = 1, bit1 = 2, both = bit0 + bit1) @@ -294,6 +302,63 @@ TEST_CASE(nested_enum_class) EXPECT(test::throws([] { migraphx::from_string("km"); })); } +TEST_CASE(stream_unscoped_enum) +{ + std::ostringstream ss; + ss << red << ' ' << green << ' ' << blue; + // The generated overload is an exact match, so it wins over the conversion to int: green + // streams as its name and not as 5. + EXPECT(ss.str() == "red green blue"); +} + +TEST_CASE(stream_scoped_enum) +{ + std::ostringstream ss; + ss << scoped_color::magenta; + EXPECT(ss.str() == "magenta"); +} + +TEST_CASE(stream_nested_enum) +{ + std::ostringstream ss; + ss << gadget::on; + EXPECT(ss.str() == "on"); +} + +TEST_CASE(stream_nested_enum_class) +{ + std::ostringstream ss; + ss << gadget::unit::cm; + EXPECT(ss.str() == "cm"); +} + +TEST_CASE(stream_enum_in_migraphx_namespace) +{ + // The generated body calls to_string unqualified, which must not be ambiguous with the + // migraphx::to_string(const T&) template for an enum declared inside namespace migraphx. + std::ostringstream ss; + ss << migraphx::busy; + EXPECT(ss.str() == "busy"); +} + +TEST_CASE(stream_unknown_value_throws) +{ + EXPECT(test::throws([] { + std::ostringstream ss; + ss << static_cast(4); + })); +} + +TEST_CASE(stream_range_of_named_enums) +{ + // How a reflected vector-of-enum operator attribute reaches the printed IR: stream_range + // streams each element with a plain os << x. + std::vector units = {gadget::unit::mm, gadget::unit::m}; + std::ostringstream ss; + ss << migraphx::stream_range(units); + EXPECT(ss.str() == "mm, m"); +} + TEST_CASE(is_named_enum_trait) { // True for every MIGRAPHX_ENUM variant, including nested ones.