Skip to content

Commit

Permalink
#0: Add StrongTypeHandle to help creating non-clashing alias types (#…
Browse files Browse the repository at this point in the history
…16309)

### Ticket
N/A

### Problem description
In tt-metal, primitive types such as `uint32_t` / `int` often get used
to alias distinct meaningful concepts like `DeviceId`, `chip_id`,
`ethernet_channel_t`, etc. This is error prone, as this allows
interchangeable uses with their underlying types.

### What's changed
Add `StrongType` as a way to create strong aliases that are incompatible
with each other:
* Integrated with `std::hash` and `std::ostream` (if the underlying type
supports these).
* Tests / documentation.

### Checklist
- [X] [Build post
commit](https://github.com/tenstorrent/tt-metal/actions/runs/12486159245)
- [X] New/Existing tests provide coverage for changes
  • Loading branch information
omilyutin-tt authored Dec 27, 2024
1 parent 2a86ff7 commit 7949c86
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 0 deletions.
1 change: 1 addition & 0 deletions tests/tt_metal/tt_metal/stl/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
set(UNIT_TESTS_STL_SRC
${CMAKE_CURRENT_SOURCE_DIR}/test_any_range.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test_slotmap.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test_strong_type.cpp
)

add_executable(unit_tests_stl ${UNIT_TESTS_STL_SRC})
Expand Down
68 changes: 68 additions & 0 deletions tests/tt_metal/tt_metal/stl/test_strong_type.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// SPDX-FileCopyrightText: © 2024 Tenstorrent Inc.
//
// SPDX-License-Identifier: Apache-2.0

#include <gtest/gtest.h>
#include <gmock/gmock.h>

#include <unordered_set>

#include "tt_metal/tt_stl/strong_type.hpp"

using MyIntId = tt::stl::StrongType<int, struct MyIntIdTag>;
using MyStringId = tt::stl::StrongType<std::string, struct MyStringIdTag>;

namespace tt::stl {
namespace {

using ::testing::ElementsAre;
using ::testing::IsNull;
using ::testing::UnorderedElementsAre;

TEST(StrongTypeTest, Basic) {
MyIntId my_int_id1(42);
MyIntId my_int_id2(43);

EXPECT_EQ(*my_int_id1, 42);
EXPECT_LT(*my_int_id1, *my_int_id2);

my_int_id1 = MyIntId(43);
EXPECT_EQ(my_int_id1, my_int_id2);
}

TEST(StrongTypeTest, UseInContainers) {
std::unordered_set<MyIntId> unordered;
std::set<MyIntId> ordered;

unordered.insert(MyIntId(42));
unordered.insert(MyIntId(43));

ordered.insert(MyIntId(1));
ordered.insert(MyIntId(2));
ordered.insert(MyIntId(3));

EXPECT_THAT(unordered, UnorderedElementsAre(MyIntId(42), MyIntId(43)));
EXPECT_THAT(ordered, ElementsAre(MyIntId(1), MyIntId(2), MyIntId(3)));
}

TEST(StrongTypeTest, StreamingOperator) {
std::stringstream ss;
ss << MyStringId("hello world");
EXPECT_EQ(ss.str(), "hello world");
}

TEST(StrongTypeTest, MoveOnlyType) {
using MoveOnlyType = StrongType<std::unique_ptr<int>, struct MoveOnlyTag>;

MoveOnlyType from(std::make_unique<int>(42));
EXPECT_EQ(**from, 42);

MoveOnlyType to = std::move(from);

// NOLINTNEXTLINE(bugprone-use-after-move)
EXPECT_THAT(*from, IsNull());
EXPECT_EQ(**to, 42);
}

} // namespace
} // namespace tt::stl
82 changes: 82 additions & 0 deletions tt_metal/tt_stl/strong_type.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// SPDX-FileCopyrightText: © 2024 Tenstorrent Inc.
//
// SPDX-License-Identifier: Apache-2.0

#pragma once

#include <utility>

namespace tt::stl {

// `StrongType` provides a strongly-typed wrapper around a value to prevent accidental type conversions.
//
// This is useful when creating aliases that rely on a primitive type; for example instead of using `uint32_t` as
// `DeviceId` directly, wrap in `StrongType<uint32_t, struct DeviceIdTag>` to prevent accidental assignment
// from `uint32_t`. Here, the 'tag' is used to disambiguate the type, and to create distinct wrappers relying on
// `uint32_t`.
//
//
// Example usage:
//
// // Create strong types.
// // `struct`s for the tag can be supplied as shown, despite of being incomplete:
//
// using UserId = StrongType<uint32_t, struct UserIdTag>;
// using GroupId = StrongType<uint32_t, struct GroupIdTag>;
// using Username = StrongType<std::string, struct UsernameTag>;
//
//
// // The different types cannot be assigned to each other:
// UserId user_id(42);
// GroupId group_id(45);
// user_id = group_id; // does not compile!
//
// Username name("john_doe");
// name = "jane_doe"; // does not compile!
// name = Username("jane_doe"); // instantiate explicitly.
//
// // Access the underlying value:
// uint32_t raw_user_id = *user_id;
// assert(*user_id < *group_id);
//
// // Strong types work with standard containers and the streaming operator, as long as the underlying type is
// // hashable and comparable.
//
// std::unordered_set<UserId> user_set;
// user_set.insert(UserId(1));
// user_set.insert(UserId(2));

// std::map<UserId, Username> user_map;
// user_map.emplace(UserId(1), Username("John Doe"));
//
// std::cout << user_map.at(UserId(1)) << std::endl; // "John Doe"
//
template <typename T, typename Tag>
class StrongType {
public:
explicit StrongType(T v) : value_(std::move(v)) {}

StrongType(const StrongType&) = default;
StrongType(StrongType&&) = default;
StrongType& operator=(const StrongType&) = default;
StrongType& operator=(StrongType&&) = default;

const T& operator*() const { return value_; }

auto operator<=>(const StrongType&) const = default;

private:
T value_;
};

} // namespace tt::stl

template <typename T, typename Tag>
std::ostream& operator<<(std::ostream& os, const tt::stl::StrongType<T, Tag>& h) {
return os << *h;
}

template <typename T, typename Tag>
struct std::hash<tt::stl::StrongType<T, Tag>> {
std::size_t operator()(const tt::stl::StrongType<T, Tag>& h) const noexcept { return std::hash<T>{}(*h); }
};

0 comments on commit 7949c86

Please sign in to comment.