Repository: tenstorrent/tt-metal
Pull request #21674: tt_stl/strong_type constexpr and unique tags
Author: login: ThisIsFineTM
Created: 2025-05-06T04:28:06Z | Merged: 2025-05-07T02:14:50Z

### Ticket
Closes #21664.

### Problem description
tt_stl strong_type didn't have constexpr, a default initializer, and a guarenteed uniqueness default.

### What's changed
Added more constexpr, added a default Tag guarenteeing uniqueness, and added a conditional default initializer (the lack of which was causing a build error at some point during a different refactor).

### Checklist
- [ ] [All post commit](https://github.com/tenstorrent/tt-metal/actions/workflows/all-post-commit-workflows.yaml) CI passes
- [ ] [Blackhole Post commit](https://github.com/tenstorrent/tt-metal/actions/workflows/blackhole-post-commit.yaml) CI with demo tests passes (if applicable)
- [ ] [Model regression](https://github.com/tenstorrent/tt-metal/actions/workflows/perf-models.yaml) CI passes (if applicable)
- [ ] [Device performance regression](https://github.com/tenstorrent/tt-metal/actions/workflows/perf-device-models.yaml) CI passes (if applicable)
- [ ] **(For models and ops writers)** Full [new models tests](https://github.com/tenstorrent/tt-metal/actions/workflows/full-new-models-suite.yaml) CI passes (if applicable)
- [x] New/Existing tests provide coverage for changes

Linked issue #21664: Modernize strong_type
### Is your feature request related to a problem? Please describe

`tt_stl/strong_type.hpp` can be made more aggressively `constexpr`, which can enable more compiler optimizations. Also in my work cleaning up some of the code in the ttnn/cpp source tree, I ran into a build error around some template code that stemmed from there being no default initializer, so I'm adding a conditional default init as well.

### Describe the solution you'd like

`constexpr` (I'm putting in a PR).

### Describe alternatives you've considered

N/A

### Additional context

N/A

Diff:
diff --git a/tests/tt_metal/tt_metal/stl/test_strong_type.cpp b/tests/tt_metal/tt_metal/stl/test_strong_type.cpp
index 86ac576bf0a0..ed1e95a0ef4f 100644
--- a/tests/tt_metal/tt_metal/stl/test_strong_type.cpp
+++ b/tests/tt_metal/tt_metal/stl/test_strong_type.cpp
@@ -34,6 +34,19 @@ TEST(StrongTypeTest, Basic) {
     EXPECT_EQ(my_int_id1, my_int_id2);
 }
 
+TEST(StrongTypeTest, GuarenteedUnique) {
+    StrongType<int> one{1};
+    StrongType<int> otherone{1};
+
+    static_assert(not std::same_as<decltype(one), decltype(otherone)>);
+    static_assert(not std::is_convertible_v<decltype(one), decltype(otherone)>);
+
+    auto runtime_same = std::is_same_v<decltype(one), decltype(otherone)>;
+    EXPECT_FALSE(runtime_same);
+
+    EXPECT_EQ(*one, *otherone);
+}
+
 TEST(StrongTypeTest, UseInContainers) {
     std::unordered_set<MyIntId> unordered;
     std::set<MyIntId> ordered;
diff --git a/tt_stl/tt_stl/strong_type.hpp b/tt_stl/tt_stl/strong_type.hpp
index 0121292adeab..afb16f568319 100644
--- a/tt_stl/tt_stl/strong_type.hpp
+++ b/tt_stl/tt_stl/strong_type.hpp
@@ -53,24 +53,49 @@ namespace tt::stl {
 //
 // std::cout << user_map.at(UserId(1)) << std::endl;  // "John Doe"
 //
-template <typename T, typename Tag>
+
+template <typename T>
+concept HasConstexprThreeWayCompare = requires(T t) {
+    { std::bool_constant<(T{}.operator<=>(T{}), true)>() } -> std::same_as<std::true_type>;
+};
+
+// lambdas are guarenteed to be unique according to the standard,
+// so the default Tag allows each StrongType instantiation
+// to be truly unique
+template <typename T, typename Tag = decltype([]() {})>
 class StrongType {
 public:
     using value_type = T;
+    using tag_type = Tag;
+
+    constexpr StrongType() noexcept
+        requires std::default_initializable<T>
+        : value_(T{}) {}
+    constexpr explicit StrongType(T v) noexcept : value_(std::move(v)) {}
 
-    constexpr explicit StrongType(T v) : value_(std::move(v)) {}
+    constexpr StrongType(const StrongType&) = default;
+    constexpr StrongType(StrongType&&) noexcept = default;
+    constexpr StrongType& operator=(const StrongType&) = default;
+    constexpr StrongType& operator=(StrongType&&) noexcept = default;
 
-    StrongType(const StrongType&) = default;
-    StrongType(StrongType&&) = default;
-    StrongType& operator=(const StrongType&) = default;
-    StrongType& operator=(StrongType&&) = default;
+    constexpr const T& operator*() const { return value_; }
 
-    const T& operator*() const { return value_; }
+    // requires() = default on a constexpr function doesn't behave on gcc/clang
+    // so it needed the explicit definition to compile.
+    // We need the separate definition for non/constexpr to accomodate
+    // types that don't have constexpr <=> (eg. std::unique_ptr)
+    constexpr auto operator<=>(const StrongType& rhs) const noexcept
+        requires(HasConstexprThreeWayCompare<T>)
+    {
+        return value_ <=> rhs.value_;
+    }
 
-    auto operator<=>(const StrongType&) const = default;
+    auto operator<=>(const StrongType&) const noexcept
+        requires(!HasConstexprThreeWayCompare<T>)
+    = default;
 
     static constexpr auto attribute_names = std::forward_as_tuple("value");
-    auto attribute_values() const { return std::forward_as_tuple(value_); }
+    constexpr auto attribute_values() const { return std::forward_as_tuple(value_); }
 
 private:
     T value_;


Reviews:
user: login: omilyutin-tt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T13:41:52Z
user: login: omilyutin-tt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T13:43:05Z
user: login: omilyutin-tt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T13:45:19Z
user: login: omilyutin-tt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T13:45:55Z
user: login: ThisIsFineTM
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T16:38:29Z
user: login: dmakoviichuk-tt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T16:41:30Z
user: login: ThisIsFineTM
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T16:56:29Z
user: login: ThisIsFineTM
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T17:04:25Z
user: login: ThisIsFineTM
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T17:05:15Z
user: login: omilyutin-tt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T17:54:52Z
user: login: omilyutin-tt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T17:55:05Z
user: login: tt-aho
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T18:01:21Z
user: login: omilyutin-tt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T18:02:11Z
user: login: omilyutin-tt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T18:03:41Z
user: login: ThisIsFineTM
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T19:41:25Z
user: login: ThisIsFineTM
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T19:47:06Z
user: login: omilyutin-tt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T19:49:06Z
user: login: ThisIsFineTM
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T20:21:45Z
user: login: omilyutin-tt
body: Thank you!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T21:51:10Z
user: login: afuller-TT
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T02:09:17Z

Review comments:
diff_hunk: @@ -53,24 +53,47 @@ namespace tt::stl {
 //
 // std::cout << user_map.at(UserId(1)) << std::endl;  // "John Doe"
 //
-template <typename T, typename Tag>
+
+template <typename T>
+concept HasConstexprThreeWayCompare = requires(T t) {
+    { std::bool_constant<(T{}.operator<=>(T{}), true)>() } -> std::same_as<std::true_type>;
+};
+
+// lambdas are guarenteed to be unique according to the standard,
+// so the default Tag allows each StrongType instantiation
+// to be truly unique
+template <typename T, typename Tag = decltype([]() {})>
 class StrongType {
 public:
     using value_type = T;
+    using tag_type = Tag;
+
+    constexpr StrongType() noexcept
+        requires std::default_initializable<T>
+        : value_(T{}) {}
+    constexpr explicit StrongType(T v) noexcept : value_(std::move(v)) {}
 
-    constexpr explicit StrongType(T v) : value_(std::move(v)) {}
+    constexpr StrongType(const StrongType&) = default;
+    constexpr StrongType(StrongType&&) noexcept = default;
+    constexpr StrongType& operator=(const StrongType&) = default;
+    constexpr StrongType& operator=(StrongType&&) noexcept = default;
 
-    StrongType(const StrongType&) = default;
-    StrongType(StrongType&&) = default;
-    StrongType& operator=(const StrongType&) = default;
-    StrongType& operator=(StrongType&&) = default;
+    constexpr const T& operator*() const { return value_; }
 
-    const T& operator*() const { return value_; }
+    // requires() = default on a constexpr function doesn't behave on gcc/clang
+    // so it needed the explicit definition
+    constexpr auto operator<=>(const StrongType& rhs) const noexcept
+        requires(HasConstexprThreeWayCompare<T>)
+    {
+        return value_ <=> rhs.value_;
path: tt_stl/tt_stl/strong_type.hpp
user: login: omilyutin-tt
body: Wouldn't `= default` be equivalent to this?
created_at: 2025-05-06T13:41:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -53,24 +53,47 @@ namespace tt::stl {
 //
 // std::cout << user_map.at(UserId(1)) << std::endl;  // "John Doe"
 //
-template <typename T, typename Tag>
+
+template <typename T>
+concept HasConstexprThreeWayCompare = requires(T t) {
+    { std::bool_constant<(T{}.operator<=>(T{}), true)>() } -> std::same_as<std::true_type>;
+};
+
+// lambdas are guarenteed to be unique according to the standard,
+// so the default Tag allows each StrongType instantiation
+// to be truly unique
+template <typename T, typename Tag = decltype([]() {})>
 class StrongType {
 public:
     using value_type = T;
+    using tag_type = Tag;
+
+    constexpr StrongType() noexcept
+        requires std::default_initializable<T>
+        : value_(T{}) {}
path: tt_stl/tt_stl/strong_type.hpp
user: login: omilyutin-tt
body: Do you have a use case in mind? I think it might actually be useful to not have default initializable ID types, for safety
created_at: 2025-05-06T13:43:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -53,24 +53,47 @@ namespace tt::stl {
 //
 // std::cout << user_map.at(UserId(1)) << std::endl;  // "John Doe"
 //
-template <typename T, typename Tag>
+
+template <typename T>
+concept HasConstexprThreeWayCompare = requires(T t) {
+    { std::bool_constant<(T{}.operator<=>(T{}), true)>() } -> std::same_as<std::true_type>;
+};
+
+// lambdas are guarenteed to be unique according to the standard,
+// so the default Tag allows each StrongType instantiation
+// to be truly unique
+template <typename T, typename Tag = decltype([]() {})>
path: tt_stl/tt_stl/strong_type.hpp
user: login: omilyutin-tt
body: Interesting work around! Do we even need a tag now? 

@dmakoviichuk-tt something we discussed just yesterday
created_at: 2025-05-06T13:45:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -34,6 +34,19 @@ TEST(StrongTypeTest, Basic) {
     EXPECT_EQ(my_int_id1, my_int_id2);
 }
 
+TEST(StrongTypeTest, GuarenteedUnique) {
+    StrongType<int> one{1};
+    StrongType<int> otherone{1};
+
+    static_assert(not std::same_as<decltype(one), decltype(otherone)>);
+    static_assert(not std::is_convertible_v<decltype(one), decltype(otherone)>);
+
+    auto runtime_same = std::is_same_v<decltype(one), decltype(otherone)>;
+    EXPECT_FALSE(runtime_same);
path: tests/tt_metal/tt_metal/stl/test_strong_type.cpp
user: login: omilyutin-tt
body: Hmm, can't this also be `static_assert`?
created_at: 2025-05-06T13:45:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -53,24 +53,47 @@ namespace tt::stl {
 //
 // std::cout << user_map.at(UserId(1)) << std::endl;  // "John Doe"
 //
-template <typename T, typename Tag>
+
+template <typename T>
+concept HasConstexprThreeWayCompare = requires(T t) {
+    { std::bool_constant<(T{}.operator<=>(T{}), true)>() } -> std::same_as<std::true_type>;
+};
+
+// lambdas are guarenteed to be unique according to the standard,
+// so the default Tag allows each StrongType instantiation
+// to be truly unique
+template <typename T, typename Tag = decltype([]() {})>
 class StrongType {
 public:
     using value_type = T;
+    using tag_type = Tag;
+
+    constexpr StrongType() noexcept
+        requires std::default_initializable<T>
+        : value_(T{}) {}
+    constexpr explicit StrongType(T v) noexcept : value_(std::move(v)) {}
 
-    constexpr explicit StrongType(T v) : value_(std::move(v)) {}
+    constexpr StrongType(const StrongType&) = default;
+    constexpr StrongType(StrongType&&) noexcept = default;
+    constexpr StrongType& operator=(const StrongType&) = default;
+    constexpr StrongType& operator=(StrongType&&) noexcept = default;
 
-    StrongType(const StrongType&) = default;
-    StrongType(StrongType&&) = default;
-    StrongType& operator=(const StrongType&) = default;
-    StrongType& operator=(StrongType&&) = default;
+    constexpr const T& operator*() const { return value_; }
 
-    const T& operator*() const { return value_; }
+    // requires() = default on a constexpr function doesn't behave on gcc/clang
+    // so it needed the explicit definition
+    constexpr auto operator<=>(const StrongType& rhs) const noexcept
+        requires(HasConstexprThreeWayCompare<T>)
+    {
+        return value_ <=> rhs.value_;
path: tt_stl/tt_stl/strong_type.hpp
user: login: ThisIsFineTM
body: Yeah, I thought so too, but gcc and clang don't like it. See: https://gcc.godbolt.org/z/vPnq4e8W7
created_at: 2025-05-06T16:38:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -53,24 +53,47 @@ namespace tt::stl {
 //
 // std::cout << user_map.at(UserId(1)) << std::endl;  // "John Doe"
 //
-template <typename T, typename Tag>
+
+template <typename T>
+concept HasConstexprThreeWayCompare = requires(T t) {
+    { std::bool_constant<(T{}.operator<=>(T{}), true)>() } -> std::same_as<std::true_type>;
+};
+
+// lambdas are guarenteed to be unique according to the standard,
+// so the default Tag allows each StrongType instantiation
+// to be truly unique
+template <typename T, typename Tag = decltype([]() {})>
path: tt_stl/tt_stl/strong_type.hpp
user: login: dmakoviichuk-tt
body: Nice!
created_at: 2025-05-06T16:41:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -53,24 +53,47 @@ namespace tt::stl {
 //
 // std::cout << user_map.at(UserId(1)) << std::endl;  // "John Doe"
 //
-template <typename T, typename Tag>
+
+template <typename T>
+concept HasConstexprThreeWayCompare = requires(T t) {
+    { std::bool_constant<(T{}.operator<=>(T{}), true)>() } -> std::same_as<std::true_type>;
+};
+
+// lambdas are guarenteed to be unique according to the standard,
+// so the default Tag allows each StrongType instantiation
+// to be truly unique
+template <typename T, typename Tag = decltype([]() {})>
 class StrongType {
 public:
     using value_type = T;
+    using tag_type = Tag;
+
+    constexpr StrongType() noexcept
+        requires std::default_initializable<T>
+        : value_(T{}) {}
path: tt_stl/tt_stl/strong_type.hpp
user: login: ThisIsFineTM
body: So one of the things I've been dabbling with is reducing coupling / improving build times: header cleanup, forward declarations, `extern template`, etc. Using `extern template` in particular led to some interesting discoveries. If you `extern template` in a header and do an explicit instantiation in a cpp file, it does a full instantiation rather than a partial instantiation based just on usage, which leads to the full template being exercised at compile time. This means that if you had an edge case that wasn't otherwised exercised, it gets flagged as a compile error because of the full instantiation. 

I found some errors being triggered due to a lack of default initialization with `std::vector<StrongType<...>>`. Vector functions such as `std::vector::resize` and `std::vector([count])` end up calling the contained type's default initializer, so not having a default initializer available can trigger really perplexing compiler errors if those codepaths are exercised in the future. It came up during a refactor and I spent enough time trying to diagnose the error that I just wanted to do away with the landmine entirely.
created_at: 2025-05-06T16:56:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -53,24 +53,47 @@ namespace tt::stl {
 //
 // std::cout << user_map.at(UserId(1)) << std::endl;  // "John Doe"
 //
-template <typename T, typename Tag>
+
+template <typename T>
+concept HasConstexprThreeWayCompare = requires(T t) {
+    { std::bool_constant<(T{}.operator<=>(T{}), true)>() } -> std::same_as<std::true_type>;
+};
+
+// lambdas are guarenteed to be unique according to the standard,
+// so the default Tag allows each StrongType instantiation
+// to be truly unique
+template <typename T, typename Tag = decltype([]() {})>
path: tt_stl/tt_stl/strong_type.hpp
user: login: ThisIsFineTM
body: Its not necessary, but you can use the tag name to make it easier to trace through compiler errors if the compiler isn't good about passing through the alias and you are diagnosing issues.
created_at: 2025-05-06T17:04:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -34,6 +34,19 @@ TEST(StrongTypeTest, Basic) {
     EXPECT_EQ(my_int_id1, my_int_id2);
 }
 
+TEST(StrongTypeTest, GuarenteedUnique) {
+    StrongType<int> one{1};
+    StrongType<int> otherone{1};
+
+    static_assert(not std::same_as<decltype(one), decltype(otherone)>);
+    static_assert(not std::is_convertible_v<decltype(one), decltype(otherone)>);
+
+    auto runtime_same = std::is_same_v<decltype(one), decltype(otherone)>;
+    EXPECT_FALSE(runtime_same);
path: tests/tt_metal/tt_metal/stl/test_strong_type.cpp
user: login: ThisIsFineTM
body: It can be, but I wanted the test suite to validate the condition at both compile and run time. I can take it out if its too redundant.
created_at: 2025-05-06T17:05:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -53,24 +53,47 @@ namespace tt::stl {
 //
 // std::cout << user_map.at(UserId(1)) << std::endl;  // "John Doe"
 //
-template <typename T, typename Tag>
+
+template <typename T>
+concept HasConstexprThreeWayCompare = requires(T t) {
+    { std::bool_constant<(T{}.operator<=>(T{}), true)>() } -> std::same_as<std::true_type>;
+};
+
+// lambdas are guarenteed to be unique according to the standard,
+// so the default Tag allows each StrongType instantiation
+// to be truly unique
+template <typename T, typename Tag = decltype([]() {})>
 class StrongType {
 public:
     using value_type = T;
+    using tag_type = Tag;
+
+    constexpr StrongType() noexcept
+        requires std::default_initializable<T>
+        : value_(T{}) {}
+    constexpr explicit StrongType(T v) noexcept : value_(std::move(v)) {}
 
-    constexpr explicit StrongType(T v) : value_(std::move(v)) {}
+    constexpr StrongType(const StrongType&) = default;
+    constexpr StrongType(StrongType&&) noexcept = default;
+    constexpr StrongType& operator=(const StrongType&) = default;
+    constexpr StrongType& operator=(StrongType&&) noexcept = default;
 
-    StrongType(const StrongType&) = default;
-    StrongType(StrongType&&) = default;
-    StrongType& operator=(const StrongType&) = default;
-    StrongType& operator=(StrongType&&) = default;
+    constexpr const T& operator*() const { return value_; }
 
-    const T& operator*() const { return value_; }
+    // requires() = default on a constexpr function doesn't behave on gcc/clang
+    // so it needed the explicit definition
+    constexpr auto operator<=>(const StrongType& rhs) const noexcept
+        requires(HasConstexprThreeWayCompare<T>)
+    {
+        return value_ <=> rhs.value_;
path: tt_stl/tt_stl/strong_type.hpp
user: login: omilyutin-tt
body: Interesting! Thanks for the godbolt. Perhaps in the future versions they will fix it.

Can you add a comment here to clarify why need both?
created_at: 2025-05-06T17:54:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -34,6 +34,19 @@ TEST(StrongTypeTest, Basic) {
     EXPECT_EQ(my_int_id1, my_int_id2);
 }
 
+TEST(StrongTypeTest, GuarenteedUnique) {
+    StrongType<int> one{1};
+    StrongType<int> otherone{1};
+
+    static_assert(not std::same_as<decltype(one), decltype(otherone)>);
+    static_assert(not std::is_convertible_v<decltype(one), decltype(otherone)>);
+
+    auto runtime_same = std::is_same_v<decltype(one), decltype(otherone)>;
+    EXPECT_FALSE(runtime_same);
path: tests/tt_metal/tt_metal/stl/test_strong_type.cpp
user: login: omilyutin-tt
body: Got it! Ok let's keep.
created_at: 2025-05-06T17:55:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -53,24 +53,47 @@ namespace tt::stl {
 //
 // std::cout << user_map.at(UserId(1)) << std::endl;  // "John Doe"
 //
-template <typename T, typename Tag>
+
+template <typename T>
+concept HasConstexprThreeWayCompare = requires(T t) {
+    { std::bool_constant<(T{}.operator<=>(T{}), true)>() } -> std::same_as<std::true_type>;
+};
+
+// lambdas are guarenteed to be unique according to the standard,
+// so the default Tag allows each StrongType instantiation
+// to be truly unique
+template <typename T, typename Tag = decltype([]() {})>
 class StrongType {
 public:
     using value_type = T;
+    using tag_type = Tag;
+
+    constexpr StrongType() noexcept
+        requires std::default_initializable<T>
+        : value_(T{}) {}
+    constexpr explicit StrongType(T v) noexcept : value_(std::move(v)) {}
 
-    constexpr explicit StrongType(T v) : value_(std::move(v)) {}
+    constexpr StrongType(const StrongType&) = default;
+    constexpr StrongType(StrongType&&) noexcept = default;
+    constexpr StrongType& operator=(const StrongType&) = default;
+    constexpr StrongType& operator=(StrongType&&) noexcept = default;
 
-    StrongType(const StrongType&) = default;
-    StrongType(StrongType&&) = default;
-    StrongType& operator=(const StrongType&) = default;
-    StrongType& operator=(StrongType&&) = default;
+    constexpr const T& operator*() const { return value_; }
 
-    const T& operator*() const { return value_; }
+    // requires() = default on a constexpr function doesn't behave on gcc/clang
+    // so it needed the explicit definition
+    constexpr auto operator<=>(const StrongType& rhs) const noexcept
path: tt_stl/tt_stl/strong_type.hpp
user: login: tt-aho
body: Are we still trying to keep metal at c++17? Does tt_stl fall under that purview? @afuller-TT @blozano-tt 
created_at: 2025-05-06T18:01:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -53,24 +53,47 @@ namespace tt::stl {
 //
 // std::cout << user_map.at(UserId(1)) << std::endl;  // "John Doe"
 //
-template <typename T, typename Tag>
+
+template <typename T>
+concept HasConstexprThreeWayCompare = requires(T t) {
+    { std::bool_constant<(T{}.operator<=>(T{}), true)>() } -> std::same_as<std::true_type>;
+};
+
+// lambdas are guarenteed to be unique according to the standard,
+// so the default Tag allows each StrongType instantiation
+// to be truly unique
+template <typename T, typename Tag = decltype([]() {})>
 class StrongType {
 public:
     using value_type = T;
+    using tag_type = Tag;
+
+    constexpr StrongType() noexcept
+        requires std::default_initializable<T>
+        : value_(T{}) {}
path: tt_stl/tt_stl/strong_type.hpp
user: login: omilyutin-tt
body: Yeah, I bumped into `std::vector` construction problem during adopting strong type for one of the ID classes.

In this case, I think it is still beneficial to disallow default construction, since these are often used for various IDs, and it often doesn't have semantic meaning. Would either of `reserve` + `push_back` or constructing a vector with a specific value work for you? I agree it is somewhat unexpected if you don't think of default constructors, but this problem isn't unique to this class.
created_at: 2025-05-06T18:02:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -53,24 +53,47 @@ namespace tt::stl {
 //
 // std::cout << user_map.at(UserId(1)) << std::endl;  // "John Doe"
 //
-template <typename T, typename Tag>
+
+template <typename T>
+concept HasConstexprThreeWayCompare = requires(T t) {
+    { std::bool_constant<(T{}.operator<=>(T{}), true)>() } -> std::same_as<std::true_type>;
+};
+
+// lambdas are guarenteed to be unique according to the standard,
+// so the default Tag allows each StrongType instantiation
+// to be truly unique
+template <typename T, typename Tag = decltype([]() {})>
 class StrongType {
 public:
     using value_type = T;
+    using tag_type = Tag;
+
+    constexpr StrongType() noexcept
+        requires std::default_initializable<T>
+        : value_(T{}) {}
+    constexpr explicit StrongType(T v) noexcept : value_(std::move(v)) {}
 
-    constexpr explicit StrongType(T v) : value_(std::move(v)) {}
+    constexpr StrongType(const StrongType&) = default;
+    constexpr StrongType(StrongType&&) noexcept = default;
+    constexpr StrongType& operator=(const StrongType&) = default;
+    constexpr StrongType& operator=(StrongType&&) noexcept = default;
 
-    StrongType(const StrongType&) = default;
-    StrongType(StrongType&&) = default;
-    StrongType& operator=(const StrongType&) = default;
-    StrongType& operator=(StrongType&&) = default;
+    constexpr const T& operator*() const { return value_; }
 
-    const T& operator*() const { return value_; }
+    // requires() = default on a constexpr function doesn't behave on gcc/clang
+    // so it needed the explicit definition
+    constexpr auto operator<=>(const StrongType& rhs) const noexcept
path: tt_stl/tt_stl/strong_type.hpp
user: login: omilyutin-tt
body: (Note this is outside of this PR)

I think if we are actually serious about cpp17, we should add a build github action that runs compilation with cpp17 standard?
created_at: 2025-05-06T18:03:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -53,24 +53,47 @@ namespace tt::stl {
 //
 // std::cout << user_map.at(UserId(1)) << std::endl;  // "John Doe"
 //
-template <typename T, typename Tag>
+
+template <typename T>
+concept HasConstexprThreeWayCompare = requires(T t) {
+    { std::bool_constant<(T{}.operator<=>(T{}), true)>() } -> std::same_as<std::true_type>;
+};
+
+// lambdas are guarenteed to be unique according to the standard,
+// so the default Tag allows each StrongType instantiation
+// to be truly unique
+template <typename T, typename Tag = decltype([]() {})>
 class StrongType {
 public:
     using value_type = T;
+    using tag_type = Tag;
+
+    constexpr StrongType() noexcept
+        requires std::default_initializable<T>
+        : value_(T{}) {}
path: tt_stl/tt_stl/strong_type.hpp
user: login: ThisIsFineTM
body: To me at least, the name "StrongType" just means that its something that should behave like the type it wraps while blocking implicit conversions. A dedicated identifier that requires initialization should probably be its own type imho. Maybe StrongID? Could compromise and do something like:
```cpp
struct StrongWrapperDefaultInitPolicy;
struct StrongWrapperNoDefaultInitPolicy;
template <typename Policy, typename T, typename Tag>
class StrongWrapper {
...
constexpr StrongWrapper() noexcept
        requires(std::default_initializable<T> && std::same_as<Policy,StrongWrapperDefaultInitPolicy>)
        : value_(T{}) {}
...
}
template <typename T, typename Tag = decltype([](){})>
using StrongType = StrongWrapper<StrongWrapperDefaultInitPolicy, T, Tag>;
template <typename T, typename Tag = decltype([](){})>
using StrongID = StrongWrapper<StrongWrapperNoDefaultInitPolicy, T, Tag>;
```
created_at: 2025-05-06T19:41:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -53,24 +53,47 @@ namespace tt::stl {
 //
 // std::cout << user_map.at(UserId(1)) << std::endl;  // "John Doe"
 //
-template <typename T, typename Tag>
+
+template <typename T>
+concept HasConstexprThreeWayCompare = requires(T t) {
+    { std::bool_constant<(T{}.operator<=>(T{}), true)>() } -> std::same_as<std::true_type>;
+};
+
+// lambdas are guarenteed to be unique according to the standard,
+// so the default Tag allows each StrongType instantiation
+// to be truly unique
+template <typename T, typename Tag = decltype([]() {})>
 class StrongType {
 public:
     using value_type = T;
+    using tag_type = Tag;
+
+    constexpr StrongType() noexcept
+        requires std::default_initializable<T>
+        : value_(T{}) {}
+    constexpr explicit StrongType(T v) noexcept : value_(std::move(v)) {}
 
-    constexpr explicit StrongType(T v) : value_(std::move(v)) {}
+    constexpr StrongType(const StrongType&) = default;
+    constexpr StrongType(StrongType&&) noexcept = default;
+    constexpr StrongType& operator=(const StrongType&) = default;
+    constexpr StrongType& operator=(StrongType&&) noexcept = default;
 
-    StrongType(const StrongType&) = default;
-    StrongType(StrongType&&) = default;
-    StrongType& operator=(const StrongType&) = default;
-    StrongType& operator=(StrongType&&) = default;
+    constexpr const T& operator*() const { return value_; }
 
-    const T& operator*() const { return value_; }
+    // requires() = default on a constexpr function doesn't behave on gcc/clang
+    // so it needed the explicit definition
+    constexpr auto operator<=>(const StrongType& rhs) const noexcept
path: tt_stl/tt_stl/strong_type.hpp
user: login: ThisIsFineTM
body: StrongWrapper already uses `operator<=>`, which is a C++20 feature. There is quite a nontrivial amount of `concept`s, named initializer, and other C++20 use already as well. Getting all of the C++20 out of the codebase would take a fairly sizable effort. It is not trivial to remove concept use without either sacrificing the additional safety concepts provide or going down the SFINAE/enable_if nightmare hole.
created_at: 2025-05-06T19:47:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -53,24 +53,47 @@ namespace tt::stl {
 //
 // std::cout << user_map.at(UserId(1)) << std::endl;  // "John Doe"
 //
-template <typename T, typename Tag>
+
+template <typename T>
+concept HasConstexprThreeWayCompare = requires(T t) {
+    { std::bool_constant<(T{}.operator<=>(T{}), true)>() } -> std::same_as<std::true_type>;
+};
+
+// lambdas are guarenteed to be unique according to the standard,
+// so the default Tag allows each StrongType instantiation
+// to be truly unique
+template <typename T, typename Tag = decltype([]() {})>
 class StrongType {
 public:
     using value_type = T;
+    using tag_type = Tag;
+
+    constexpr StrongType() noexcept
+        requires std::default_initializable<T>
+        : value_(T{}) {}
path: tt_stl/tt_stl/strong_type.hpp
user: login: omilyutin-tt
body: Hmm, thats fair. Let's just add a default I think, either way it is relatively minor, but avoids extra code and extra ways to use (and misuse).

Thanks for the discussion!
created_at: 2025-05-06T19:49:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -53,24 +53,47 @@ namespace tt::stl {
 //
 // std::cout << user_map.at(UserId(1)) << std::endl;  // "John Doe"
 //
-template <typename T, typename Tag>
+
+template <typename T>
+concept HasConstexprThreeWayCompare = requires(T t) {
+    { std::bool_constant<(T{}.operator<=>(T{}), true)>() } -> std::same_as<std::true_type>;
+};
+
+// lambdas are guarenteed to be unique according to the standard,
+// so the default Tag allows each StrongType instantiation
+// to be truly unique
+template <typename T, typename Tag = decltype([]() {})>
 class StrongType {
 public:
     using value_type = T;
+    using tag_type = Tag;
+
+    constexpr StrongType() noexcept
+        requires std::default_initializable<T>
+        : value_(T{}) {}
+    constexpr explicit StrongType(T v) noexcept : value_(std::move(v)) {}
 
-    constexpr explicit StrongType(T v) : value_(std::move(v)) {}
+    constexpr StrongType(const StrongType&) = default;
+    constexpr StrongType(StrongType&&) noexcept = default;
+    constexpr StrongType& operator=(const StrongType&) = default;
+    constexpr StrongType& operator=(StrongType&&) noexcept = default;
 
-    StrongType(const StrongType&) = default;
-    StrongType(StrongType&&) = default;
-    StrongType& operator=(const StrongType&) = default;
-    StrongType& operator=(StrongType&&) = default;
+    constexpr const T& operator*() const { return value_; }
 
-    const T& operator*() const { return value_; }
+    // requires() = default on a constexpr function doesn't behave on gcc/clang
+    // so it needed the explicit definition
+    constexpr auto operator<=>(const StrongType& rhs) const noexcept
+        requires(HasConstexprThreeWayCompare<T>)
+    {
+        return value_ <=> rhs.value_;
path: tt_stl/tt_stl/strong_type.hpp
user: login: ThisIsFineTM
body: Sure. I'll update the branch and squash.
created_at: 2025-05-06T20:21:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -53,24 +53,47 @@ namespace tt::stl {
 //
 // std::cout << user_map.at(UserId(1)) << std::endl;  // "John Doe"
 //
-template <typename T, typename Tag>
+
+template <typename T>
+concept HasConstexprThreeWayCompare = requires(T t) {
+    { std::bool_constant<(T{}.operator<=>(T{}), true)>() } -> std::same_as<std::true_type>;
+};
+
+// lambdas are guarenteed to be unique according to the standard,
+// so the default Tag allows each StrongType instantiation
+// to be truly unique
+template <typename T, typename Tag = decltype([]() {})>
 class StrongType {
 public:
     using value_type = T;
+    using tag_type = Tag;
+
+    constexpr StrongType() noexcept
+        requires std::default_initializable<T>
+        : value_(T{}) {}
+    constexpr explicit StrongType(T v) noexcept : value_(std::move(v)) {}
 
-    constexpr explicit StrongType(T v) : value_(std::move(v)) {}
+    constexpr StrongType(const StrongType&) = default;
+    constexpr StrongType(StrongType&&) noexcept = default;
+    constexpr StrongType& operator=(const StrongType&) = default;
+    constexpr StrongType& operator=(StrongType&&) noexcept = default;
 
-    StrongType(const StrongType&) = default;
-    StrongType(StrongType&&) = default;
-    StrongType& operator=(const StrongType&) = default;
-    StrongType& operator=(StrongType&&) = default;
+    constexpr const T& operator*() const { return value_; }
 
-    const T& operator*() const { return value_; }
+    // requires() = default on a constexpr function doesn't behave on gcc/clang
+    // so it needed the explicit definition
+    constexpr auto operator<=>(const StrongType& rhs) const noexcept
path: tt_stl/tt_stl/strong_type.hpp
user: login: afuller-TT
body: > Are we still trying to keep metal at c++17? Does tt_stl fall under that purview?

It was mentioned back in the day, and I haven't seen it on the roadmaps.  So until it hits a roadmap I'll chalk it up as wishful thinking.

If it DOES become a thing for TT-Metalium, then it's also going to be a thing for tt-stl transitively.

But we'll need to hear more direction on the constraints before we worry too much about it.  Eg: is it okay if the code requires C++20 to _compile_ but then it can run with a C++17 runtime?  Or must it be buildable from source in a C++17 environment?  These are question we'd need clarified.
created_at: 2025-05-07T02:09:17Z
author_association: CONTRIBUTOR

PR comments:
user: login: omilyutin-tt
created_at: 2025-05-06T19:52:07Z
body: @ThisIsFineTM I kicked-off CI pipelines, thanks! All looks good, I'd just expand in a comment on the need of the trick with defaulting spaceship operator. Many hands would itch to "fix" this otherwise and try to remove to see how it breaks:)
author_association: CONTRIBUTOR
user: login: ThisIsFineTM
created_at: 2025-05-06T21:22:42Z
body: @omilyutin-tt Added the additional comment, squashed, and rebased onto latest main.
author_association: CONTRIBUTOR
user: login: omilyutin-tt
created_at: 2025-05-06T21:51:37Z
body: Will merge as soon as tests are passing
author_association: CONTRIBUTOR