Rust 39 🦀 Trait Bounds and Conditional Method Implementations
A generic type parameter is a placeholder, but a placeholder with no constraints is nearly useless. A function that takes T without bounds can only move, borrow, and drop the value. It cannot print it, compare it, clone it, or call any method beyond what every type supports. Trait bounds are the constraints that make generics useful. T: Display guarantees that the value can be formatted. T: PartialOrd guarantees that two values can be compared. T: Clone guarantees that the value can be duplicated. The bound is a promise from the caller to the generic code: “whatever type I pass, it satisfies this contract.”
Trait bounds appear in several positions. They can be written inline on a type parameter (<T: Display>), in a where clause (where T: Display), on a struct definition, on an impl block, or on a method. The position determines the scope of the bound. A bound on the struct definition requires the bound for every use of the struct. A bound on the impl block requires the bound only for the methods in that block. A bound on a method requires the bound only for that method. This layering is what makes conditional method implementations possible: a method that is available only when the type parameter satisfies a specific bound.
Conditional method implementations are the mechanism behind some of Rust’s most important patterns. Vec<T> has a push method for all T, but it has a sort method only when T: Ord. Option<T> has an unwrap method for all T, but it has a map method that transforms the inner value with a function, and the function’s return type can be a different type. The standard library is built on conditional implementations, and understanding them is what allows a custom type to have methods that are available only under the right conditions.
This chapter covers three areas. First, why trait bounds exist — the problem of unconstrained generics, the difference between a bound that is required and a bound that is optional, and the cost of over-constraining. Second, how trait bounds are written — inline bounds, where clauses, multiple bounds with +, and the ?Sized escape hatch. Third, how conditional method implementations work — bounds on impl blocks, bounds on individual methods, and the pattern of layering methods from the most general to the most specific. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing bound scope and method availability.
Key point: A trait bound constrains a generic type parameter to types that implement a trait. Bounds can be placed on type parameters, struct definitions, impl blocks, or individual methods. The position determines which code requires the bound. A bound on an impl block makes the methods in that block available only when the type parameter satisfies the bound, which is the basis for conditional method implementations .
Why trait bounds and conditional implementations exist
The unconstrained-generic problem. A generic function without bounds is limited to operations that work for every type. It cannot call clone, cannot use +, cannot format with {}, and cannot compare with ==. The compiler has no information about what the type can do, so it conservatively rejects any operation that is not universal. Trait bounds provide the missing information. T: Clone tells the compiler that clone is available. T: Display tells it that {} formatting is available. The bound is a compile-time guarantee that the operation is valid for every type the function accepts .
The over-constraining problem. A bound that is too strong excludes types that would otherwise work. A function that only calls clone on its parameter should require T: Clone, not T: Clone + Display. Requiring Display is unnecessary and prevents the function from being used with types that are cloneable but not displayable. The general principle is to require the minimum bound necessary for the function body to compile. This maximizes the set of types the function can accept and minimizes the coupling between the function and its callers .
The optional-method problem. Some methods make sense only for some types. Vec::sort requires T: Ord, because sorting needs a total ordering. Vec::push requires nothing, because adding an element needs no capabilities. If sort were required for all Vec<T>, then Vec<T> could not exist for types without Ord. The solution is conditional implementation: impl<T: Ord> Vec<T> { fn sort(&mut self) { ... } }. The method is available only when T satisfies the bound. Types that do not implement Ord still have a Vec, but without the sort method. This is the mechanism that allows a type to have a general interface and a specialized interface simultaneously .
The layering problem. A type often has methods at multiple levels of specificity. Vec<T> has methods available for all T (like len and push), methods available when T: Clone (like extend_from_slice), and methods available when T: Ord (like sort). Each level is a separate impl block with its own bounds. The blocks do not conflict; they accumulate. A Vec<i32> has all three levels of methods, because i32 implements Clone and Ord. A Vec<SomeNonOrdType> has the first and second levels but not the third. The layering is how the standard library provides a rich interface without requiring every type to satisfy every bound .
The Sized problem. By default, every type parameter has an implicit Sized bound, meaning the type must have a known size at compile time. This is required for most operations, because the compiler must allocate the right amount of memory. But some types — slices, trait objects, and strings — are unsized, and they can still be used behind references. The ?Sized bound removes the implicit Sized requirement, allowing the generic code to accept unsized types. This is how Box<T> can hold a dyn Trait, and how &[T] can be passed to a function that takes T: ?Sized. The ?Sized escape hatch is necessary for generic code that works with references to unsized types .
The trade-off. Every bound is a constraint on the caller. A bound that is too weak means the function body cannot compile. A bound that is too strong excludes valid types. The design of trait bounds is the design of the function’s interface: what capabilities does it require, and what types should be able to use it? The answer is not always obvious, and it often changes as the function evolves. The rule of thumb is to start with the minimum bound that makes the body compile, and add bounds only when a new operation requires them.
a. Inline bounds and where clauses
Trait bounds can be written inline on a type parameter or in a where clause. The two forms are equivalent; the choice is a matter of readability.
Inline bounds:
fn print_all<T: Display>(items: &[T]) {
for item in items {
println!("{}", item);
}
}
Where clause:
fn print_all<T>(items: &[T])
where
T: Display,
{
for item in items {
println!("{}", item);
}
}
The where clause is preferred when there are multiple bounds or when the bounds are complex. It separates the type parameters from their constraints, which makes the signature easier to read .
Multiple bounds are separated by +:
fn process<T: Display + Clone>(item: T) {
let copy = item.clone();
println!("{}", item);
println!("{}", copy);
}
The + means “and”. The type must implement both Display and Clone.
The ?Sized bound removes the implicit Sized requirement:
fn print<T: Display + ?Sized>(item: &T) {
println!("{}", item);
}
This function accepts &str (which is unsized), &dyn Display, and any sized type that implements Display. Without ?Sized, the function would reject unsized types. The ?Sized bound can only be used in the ?Trait form, and it is the only trait bound with the ? prefix .
b. Bounds on structs and enums
A struct or enum definition can have trait bounds. The bound applies to every use of the type, including the fields and any methods .
struct Wrapper<T: Display> {
value: T,
}
The Wrapper<T> struct requires T: Display. This means every Wrapper must have a T that is displayable. The bound is part of the struct’s definition, so it is checked wherever the struct is used. This is usually too restrictive: the struct itself does not use Display; only the methods do. The recommendation is to place bounds on impl blocks instead of the struct definition, so that the struct can be instantiated with any type, and the methods are available only when the bound is satisfied .
struct Wrapper<T> {
value: T,
}
impl<T: Display> Wrapper<T> {
fn print(&self) {
println!("{}", self.value);
}
}
Now Wrapper<T> can hold any T, but the print method is available only when T: Display. The struct definition is unconstrained, and the method carries the bound. This is the more flexible approach and the one used throughout the standard library.
c. Conditional method implementations
A conditional implementation is an impl block with a bound. The methods in the block are available only when the type parameter satisfies the bound. Multiple impl blocks with different bounds can coexist, each providing a different set of methods.
struct Pair<T> {
first: T,
second: T,
}
// Available for all T
impl<T> Pair<T> {
fn new(first: T, second: T) -> Self {
Pair { first, second }
}
}
// Available only when T: Display + PartialOrd
impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.first >= self.second {
println!("The largest is {}", self.first);
} else {
println!("The largest is {}", self.second);
}
}
}
Pair<i32> has both new and cmp_display, because i32 implements Display and PartialOrd. Pair<SomeTypeWithoutOrd> has only new. The method availability is determined by the bounds on the impl block .
A method can also have its own bounds, independent of the impl block’s bounds:
impl<T> Pair<T> {
fn print<U: Display>(&self, extra: U) {
println!("extra: {}", extra);
}
}
The print method is available for all Pair<T>, but it requires that the extra argument implement Display. The bound is on the method, not on the impl block. This allows a method to introduce a new type parameter with its own constraints .
The pattern of layering impl blocks from the most general to the most specific is how the standard library provides a rich interface. Vec<T> has an impl<T> block with general methods, an impl<T: Clone> block with clone-dependent methods, and an impl<T: Ord> block with sort-dependent methods. Each block adds methods to the set available for a Vec with the appropriate type parameter .
Complete Example Session
// ============================================
// PART 1: INLINE BOUND
// ============================================
use std::fmt::Display;
fn print_all<T: Display>(items: &[T]) {
for item in items {
println!("{}", item);
}
}
fn main() {
print_all(&[1, 2, 3]);
print_all(&["a", "b", "c"]);
}
// ============================================
// PART 2: WHERE CLAUSE
// ============================================
fn print_all_where<T>(items: &[T])
where
T: Display,
{
for item in items {
println!("{}", item);
}
}
// Equivalent to the inline form. Preferred when
// there are multiple bounds.
// ============================================
// PART 3: MULTIPLE BOUNDS
// ============================================
fn process<T: Display + Clone>(item: T) {
let copy = item.clone();
println!("original: {}", item);
println!("copy: {}", copy);
}
// T must implement both Display and Clone.
// ============================================
// PART 4: ?SIZED BOUND
// ============================================
fn print_unsized<T: Display + ?Sized>(item: &T) {
println!("{}", item);
}
fn main() {
print_unsized("hello"); // &str is unsized
print_unsized(&42); // &i32 is sized
}
// ============================================
// PART 5: BOUND ON STRUCT DEFINITION
// ============================================
struct Wrapper<T: Display> {
value: T,
}
// The bound is on the struct. Every Wrapper must
// have a T that implements Display.
// This is usually too restrictive.
// ============================================
// PART 6: BOUND ON IMPL BLOCK
// ============================================
struct Wrapper<T> {
value: T,
}
impl<T: Display> Wrapper<T> {
fn print(&self) {
println!("{}", self.value);
}
}
// The struct is unconstrained. The method is
// available only when T: Display.
// ============================================
// PART 7: CONDITIONAL METHOD IMPLEMENTATIONS
// ============================================
struct Pair<T> {
first: T,
second: T,
}
// Available for all T
impl<T> Pair<T> {
fn new(first: T, second: T) -> Self {
Pair { first, second }
}
}
// Available only when T: Display + PartialOrd
impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.first >= self.second {
println!("The largest is {}", self.first);
} else {
println!("The largest is {}", self.second);
}
}
}
fn main() {
let pair = Pair::new(5, 10);
pair.cmp_display(); // 10
}
// ============================================
// PART 8: METHOD-LEVEL BOUNDS
// ============================================
impl<T> Pair<T> {
fn print_extra<U: Display>(&self, extra: U) {
println!("extra: {}", extra);
}
}
// print_extra is available for all Pair<T>,
// but requires U: Display for the extra argument.
// ============================================
// PART 9: THE VEC PATTERN
// ============================================
// The standard library uses conditional impls
// to provide methods at different levels.
// impl<T> Vec<T> → len, push, pop, ...
// impl<T: Clone> Vec<T> → extend_from_slice, ...
// impl<T: Ord> Vec<T> → sort, binary_search, ...
// A Vec<i32> has all three sets of methods.
// A Vec<NonOrdType> has only the first two.
// ============================================
// PART 10: THE COMPLETE PATTERN
// ============================================
struct Stack<T> {
items: Vec<T>,
}
// Available for all T
impl<T> Stack<T> {
fn new() -> Self {
Stack { items: Vec::new() }
}
fn push(&mut self, item: T) {
self.items.push(item);
}
fn pop(&mut self) -> Option<T> {
self.items.pop()
}
fn len(&self) -> usize {
self.items.len()
}
}
// Available only when T: Display
impl<T: Display> Stack<T> {
fn print(&self) {
for item in &self.items {
println!("{}", item);
}
}
}
// Available only when T: PartialOrd
impl<T: PartialOrd> Stack<T> {
fn max(&self) -> Option<&T> {
self.items.iter().max_by(|a, b| a.partial_cmp(b).unwrap())
}
}
fn main() {
let mut stack = Stack::new();
stack.push(3);
stack.push(1);
stack.push(2);
stack.print(); // 3, 1, 2
println!("{:?}", stack.max()); // Some(3)
let mut words = Stack::new();
words.push("apple");
words.push("banana");
words.print(); // apple, banana
// words.max(); // error: &str is not PartialOrd
}
The ten parts show inline bounds, where clauses, multiple bounds, ?Sized, bounds on struct definitions, bounds on impl blocks, conditional method implementations, method-level bounds, the Vec pattern, and a complete Stack example with layered implementations.
Quick Reference
Bound Positions
| Position | Syntax | Scope |
|---|---|---|
| Type parameter | <T: Trait> | The function or struct |
| Where clause | where T: Trait | The function or impl |
| Struct definition | struct S<T: Trait> | Every use of the struct |
| Impl block | impl<T: Trait> S<T> | The methods in the block |
| Method | fn f<U: Trait>(...) | The method only |
Bound Syntax
| Syntax | Meaning |
|---|---|
T: Display | T implements Display |
T: Display + Clone | T implements both |
T: ?Sized | T may be unsized |
where T: Display | Same as inline |
impl<T: Display> S<T> | Methods when T: Display |
Conditional Implementation Levels
| Impl block | Methods available |
|---|---|
impl<T> Stack<T> | For all T |
impl<T: Display> Stack<T> | When T: Display |
impl<T: PartialOrd> Stack<T> | When T: PartialOrd |
impl<T: Display + PartialOrd> Stack<T> | When both |
The Vec Example
| Impl block | Methods |
|---|---|
impl<T> Vec<T> | len, push, pop, is_empty |
impl<T: Clone> Vec<T> | extend_from_slice, clone |
impl<T: Ord> Vec<T> | sort, binary_search |
Best Practices
✅ Do This:
// Place bounds on impl blocks, not structs
struct Wrapper<T> { value: T } // ✅
impl<T: Display> Wrapper<T> { fn print(&self) { ... } } // ✅
// Use where clauses for multiple bounds
fn f<T>(x: T) where T: Display + Clone { ... } // ✅
// Require the minimum bound needed
fn clone_it<T: Clone>(x: &T) -> T { x.clone() } // ✅
// Layer impl blocks by specificity
impl<T> Stack<T> { fn new() -> Self { ... } } // ✅
impl<T: Display> Stack<T> { fn print(&self) { ... } } // ✅
// Use ?Sized for references to unsized types
fn print<T: Display + ?Sized>(x: &T) { ... } // ✅
❌ Don’t Do This:
// Don't put unnecessary bounds on structs
struct Wrapper<T: Display> { value: T } // too restrictive // ❌
// Don't over-constrain functions
fn clone_it<T: Clone + Display + Debug>(x: &T) -> T { x.clone() } // ❌
// Don't duplicate bounds in every method
impl<T> S<T> { fn a<T: Display>(...) {} fn b<T: Display>(...) {} } // ❌
// Don't forget ?Sized when accepting &str
fn f<T: Display>(x: &T) {} // rejects &str // ❌
// Don't put method-specific bounds on the impl
impl<T: Display> S<T> { fn only_for_u<U>(...) {} } // U not bound // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Method not available | Missing bound on impl or method | Add the required bound |
| Struct too restrictive | Bound on struct definition | Move bound to impl |
&str rejected | Missing ?Sized | Add ?Sized to the bound |
| Conflicting impls | Overlapping bounds | Use disjoint bounds or a single impl |
| Bound not satisfied | Type doesn’t implement trait | Implement the trait or use a different type |
where clause syntax error | Misplaced comma or colon | Check the syntax |
| Method-level bound shadows impl bound | Same type parameter name | Use a different name |
Real-World Examples
1. Inline Bound
fn print<T: Display>(x: T) { println!("{}", x); }
2. Where Clause
fn print<T>(x: T) where T: Display { println!("{}", x); }
3. Multiple Bounds
fn f<T: Display + Clone>(x: T) { }
4. ?Sized
fn print<T: Display + ?Sized>(x: &T) { }
5. Bound on Impl
impl<T: Display> Wrapper<T> { fn print(&self) { } }
6. Conditional Method
impl<T: Ord> Vec<T> { fn sort(&mut self) { } }
7. Method-Level Bound
impl<T> Pair<T> { fn show<U: Display>(&self, u: U) { } }
8. Layered Impls
impl<T> Stack<T> { fn new() -> Self { } }
impl<T: Display> Stack<T> { fn print(&self) { } }
9. Bound on Struct
struct Wrapper<T: Display> { value: T }
10. The Vec Pattern
impl<T> Vec<T> { } impl<T: Clone> Vec<T> { } impl<T: Ord> Vec<T> { }
Visual
Bound Scope
┌──────────────────────────────────────────────────────────────┐
│ BOUND SCOPE │
│ │
│ BOUND ON STRUCT DEFINITION: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ struct Wrapper<T: Display> { value: T } │ │
│ │ │ │
│ │ Every Wrapper must have T: Display. │ │
│ │ Cannot create Wrapper<NonDisplayType>. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ BOUND ON IMPL BLOCK: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ struct Wrapper<T> { value: T } │ │
│ │ impl<T: Display> Wrapper<T> { fn print(&self) { } } │ │
│ │ │ │
│ │ Can create Wrapper<AnyType>. │ │
│ │ print() available only when T: Display. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ BOUND ON METHOD: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ impl<T> Wrapper<T> { │ │
│ │ fn show<U: Display>(&self, u: U) { } │ │
│ │ } │ │
│ │ │ │
│ │ show() available for all Wrapper<T>. │ │
│ │ Requires U: Display for the argument. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
Conditional Method Availability
┌──────────────────────────────────────────────────────────────┐
│ CONDITIONAL METHOD AVAILABILITY │
│ │
│ struct Stack<T> { items: Vec<T> } │
│ │
│ impl<T> Stack<T> { │
│ fn new() -> Self { } │
│ fn push(&mut self, item: T) { } │
│ fn pop(&mut self) -> Option<T> { } │
│ } │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Available for ALL Stack<T>: │ │
│ │ Stack<i32> → new, push, pop │ │
│ │ Stack<NonOrdType> → new, push, pop │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ impl<T: Display> Stack<T> { │
│ fn print(&self) { } │
│ } │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Available when T: Display: │ │
│ │ Stack<i32> → print │ │
│ │ Stack<NonDisplay> → ERROR: method not found │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ impl<T: PartialOrd> Stack<T> { │
│ fn max(&self) -> Option<&T> { } │
│ } │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Available when T: PartialOrd: │ │
│ │ Stack<i32> → max │ │
│ │ Stack<NonOrd> → ERROR: method not found │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
The Vec Layering
┌──────────────────────────────────────────────────────────────┐
│ THE VEC LAYERING │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ impl<T> Vec<T> │ │
│ │ len, push, pop, is_empty, iter, ... │ │
│ │ │ │
│ │ Available for ALL Vec<T> │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ impl<T: Clone> Vec<T> │ │
│ │ extend_from_slice, resize, ... │ │
│ │ │ │
│ │ Available when T: Clone │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ impl<T: Ord> Vec<T> │ │
│ │ sort, binary_search, ... │ │
│ │ │ │
│ │ Available when T: Ord │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ Vec<i32> has all three layers. │
│ Vec<NonOrdType> has the first two. │
│ Vec<NonCloneType> has only the first. │
│ │
└──────────────────────────────────────────────────────────────┘
?Sized Bound
┌──────────────────────────────────────────────────────────────┐
│ ?SIZED BOUND │
│ │
│ WITHOUT ?Sized: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ fn print<T: Display>(x: &T) { } │ │
│ │ │ │
│ │ T must be Sized (implicit bound). │ │
│ │ print(&42) → OK (i32 is Sized) │ │
│ │ print("hello") → ERROR (&str is unsized) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ WITH ?Sized: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ fn print<T: Display + ?Sized>(x: &T) { } │ │
│ │ │ │
│ │ T may be Sized or unsized. │ │
│ │ print(&42) → OK │ │
│ │ print("hello") → OK (&str is unsized) │ │
│ │ print(&dyn Display) → OK (trait object is unsized) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ?Sized removes the implicit Sized bound. It is the only │
│ trait bound with the ? prefix. │
│ │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Trait bound | Constraint on a generic type parameter |
| Inline bound | <T: Trait> |
| Where clause | where T: Trait |
| Multiple bounds | <T: Trait1 + Trait2> |
?Sized | Removes the implicit Sized bound |
| Bound on struct | Restricts every use of the struct |
| Bound on impl | Restricts the methods in the block |
| Bound on method | Restricts only that method |
| Conditional implementation | impl<T: Trait> Type<T> |
| Layered impls | Multiple blocks with different bounds |
Key takeaways:
- Trait bounds make generics useful. Without bounds, a generic function can only move, borrow, and drop. With bounds, it can call methods, compare values, and format output. The bound is a compile-time guarantee that the operation is valid .
- The bound position determines the scope. A bound on a struct definition restricts every use of the struct. A bound on an
implblock restricts the methods in that block. A bound on a method restricts only that method. Place bounds as narrowly as possible . - Prefer bounds on
implblocks over struct definitions. A bound on the struct makes the struct unusable with types that do not satisfy the bound, even when the bound is not needed. A bound on theimplblock keeps the struct general and restricts only the methods that need the bound . - Conditional implementations layer methods by capability.
impl<T>provides methods for all types.impl<T: Display>adds methods for displayable types.impl<T: Ord>adds methods for orderable types. Each layer is independent, and a type with the required traits gets all the layers . whereclauses are preferred for complex bounds. Thewhereclause separates the type parameters from their constraints, which makes the signature easier to read when there are multiple bounds or when the bounds are long .?Sizedallows unsized types. By default, every type parameter is implicitlySized. The?Sizedbound removes this requirement, allowing the generic code to accept references to unsized types likestranddyn Trait.- Require the minimum bound necessary. A bound that is too strong excludes valid types. A bound that is too weak fails to compile. Start with the minimum and add only when the body requires it .
- The standard library uses conditional implementations extensively.
Vec<T>has methods at multiple levels of specificity.Option<T>andResult<T, E>use bounds for methods likeunwrapandmap. The pattern is fundamental to Rust’s design.
Remember: Trait bounds are the constraints that make generics usable. They tell the compiler what operations are available for a type parameter, and they allow the generic code to call those operations. The position of the bound determines its scope: struct bounds restrict everything, impl bounds restrict the methods in the block, and method bounds restrict only that method. Conditional implementations layer methods by capability, so a type has a general interface for all type parameters and a specialized interface for those that satisfy additional bounds. The ?Sized bound is the escape hatch for unsized types. The design of bounds is the design of the function’s interface, and the goal is to require exactly what the body needs — no more, no less.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!