Question
In Rust, I have a struct where each field needs an initial default value when the struct is created. Right now I am using a new() function that manually assigns every field:
struct CParams {
i_insert_max: i64,
i_update_max: i64,
i_delete_max: i64,
i_instance_max: i64,
t_first_instance: bool,
t_create_tables: bool,
t_continue: bool,
}
impl CParams {
fn new() -> CParams {
CParams {
i_insert_max: -1,
i_update_max: -1,
i_delete_max: -1,
i_instance_max: -1,
t_first_instance: false,
t_create_tables: false,
t_continue: false,
}
}
}
I would prefer a shorter or more idiomatic way to give struct fields default values when the struct is instantiated. Is there a more concise approach in Rust?
Short Answer
By the end of this page, you will understand how Rust initializes struct fields, why fields cannot hold inline default values in the struct definition itself, and how to use idiomatic alternatives such as a new() constructor, the Default trait, and struct update syntax to keep initialization concise and maintainable.
Concept
In Rust, a struct definition describes the shape of the data, not the values each field should start with. That means you cannot write default field values directly inside the struct declaration like some other languages allow.
Instead, Rust uses a few common patterns:
- A constructor function such as
new() - The
Defaulttrait for standard default values - Struct update syntax to override only a few fields
Why this matters:
- It keeps type definitions separate from object creation logic.
- It makes initialization explicit.
- It scales better when your struct grows or when multiple initialization styles are needed.
For your example, manually writing all fields inside new() works, but the more idiomatic Rust approach is often to implement Default. Then you can create values with:
let params = CParams::default();
If you want mostly default values but need to change one or two fields, you can combine that with struct update syntax:
let params = CParams {
i_insert_max: 10,
..Default::default()
};
This is both shorter and easier to maintain.
Mental Model
Think of a Rust struct as a blueprint for a form.
The struct says:
- these are the fields that exist
- these are their types
But the struct does not say what values should be filled in automatically.
To actually fill out the form, you need a helper:
new()is like filling the form by hand each timeDefaultis like having a pre-filled template- struct update syntax is like taking the template and changing only a few boxes
So the struct is the form layout, and Default is the saved template.
Syntax and Examples
Basic constructor
A straightforward solution is to keep a new() method:
struct CParams {
i_insert_max: i64,
i_update_max: i64,
i_delete_max: i64,
i_instance_max: i64,
t_first_instance: bool,
t_create_tables: bool,
t_continue: bool,
}
impl CParams {
fn new() -> Self {
Self {
i_insert_max: -1,
i_update_max: -1,
i_delete_max: -1,
i_instance_max: -1,
t_first_instance: false,
t_create_tables: false,
t_continue: false,
}
}
}
This is valid and clear. Using Self is slightly shorter than repeating CParams.
Using the Default trait
A more idiomatic solution is to implement Default:
struct CParams {
i_insert_max: i64,
i_update_max: i64,
i_delete_max: i64,
i_instance_max: i64,
t_first_instance: bool,
t_create_tables: bool,
t_continue: bool,
}
impl Default for CParams {
fn default() -> Self {
Self {
i_insert_max: -1,
i_update_max: -1,
i_delete_max: -1,
i_instance_max: -1,
t_first_instance: false,
t_create_tables: false,
t_continue: false,
}
}
}
Now you can create the struct like this:
let params = CParams::default();
Override a few fields with struct update syntax
let params = CParams {
i_insert_max: 100,
t_continue: true,
..Default::default()
};
This means:
- set
i_insert_maxto100 - set
t_continuetotrue - use default values for all remaining fields
When derive(Default) works
If all fields should use their type's normal defaults, you can derive Default automatically:
#[derive(Default)]
struct Flags {
enabled: bool,
retries: i32,
}
Here:
booldefaults tofalse- integers default to
0
But in your case, several integer fields should be -1, so you need a manual Default implementation instead of derive(Default).
Step by Step Execution
Consider this example:
struct CParams {
i_insert_max: i64,
i_update_max: i64,
t_continue: bool,
}
impl Default for CParams {
fn default() -> Self {
Self {
i_insert_max: -1,
i_update_max: -1,
t_continue: false,
}
}
}
fn main() {
let params = CParams {
i_insert_max: 50,
..Default::default()
};
println!("{}", params.i_insert_max);
println!("{}", params.i_update_max);
println!("{}", params.t_continue);
}
What happens step by step
- Rust sees the
CParamsstruct definition. - Rust sees the
Defaultimplementation forCParams. - In
main, a newCParamsvalue is created. i_insert_maxis explicitly set to50.Default::default()is called to fill in the remaining fields.- So
i_update_maxbecomes-1andt_continuebecomesfalse. - The program prints:
50
-1
false
This is useful because you only write the fields you want to customize, while the rest come from a single central default definition.
Real World Use Cases
Default struct values are common in real Rust programs.
Configuration objects
A program may have settings like:
- timeout values
- retry limits
- feature flags
You can define sensible defaults and override only what is needed.
API request options
An HTTP client might use a struct like:
follow_redirects: truetimeout_secs: 30use_cache: false
Most callers use defaults. A few callers customize specific fields.
Command-line tools
A CLI app may store parsed options in a struct. If the user does not provide a flag, the program uses defaults.
Test data setup
Tests often need objects that are mostly the same. A Default implementation lets you create a base object and change only the fields relevant to a test.
Stateful application logic
Structs representing application state often need a known initial state before processing begins.
Real Codebase Usage
In real projects, developers commonly use Default in a few standard patterns.
Pattern: default configuration object
let config = AppConfig::default();
This gives a clean starting point.
Pattern: override only what matters
let config = AppConfig {
debug: true,
..Default::default()
};
This is very common in tests and setup code.
Pattern: combine new() with Default
Sometimes a type provides both:
impl MyType {
fn new() -> Self {
Self::default()
}
}
This is useful when users naturally expect a new() constructor.
Pattern: validation after construction
If some fields must follow rules, developers may avoid exposing public fields directly and instead provide constructor functions that validate values.
impl AppConfig {
fn new(timeout: i64) -> Self {
Self {
timeout,
..Default::default()
}
}
}
Pattern: test fixtures
Tests often create a default object and override a single field:
let params = CParams {
t_continue: true,
..Default::default()
};
This keeps tests short and readable.
Common Mistakes
Mistake 1: Expecting inline field defaults in the struct definition
This is not valid Rust:
struct CParams {
i_insert_max: i64 = -1,
t_continue: bool = false,
}
Rust structs declare field names and types only.
Mistake 2: Using derive(Default) when custom values are needed
#[derive(Default)]
struct CParams {
i_insert_max: i64,
t_continue: bool,
}
This gives:
i_insert_max = 0t_continue = false
If you need -1, derive(Default) is not enough. Write impl Default manually.
Mistake 3: Forgetting required fields without ..Default::default()
Broken code:
let params = CParams {
i_insert_max: 10,
};
Rust requires all fields to be initialized unless you use struct update syntax.
Correct version:
let params = CParams {
i_insert_max: 10,
..Default::default()
};
Mistake 4: Duplicating the same defaults in many places
If you manually repeat the same values across the codebase, maintenance becomes harder.
Better approach:
- put defaults in
Default - optionally call them from
new()
Mistake 5: Using unclear sentinel values
Using -1 can be valid, but ask whether it really represents "not set" clearly.
Sometimes a better design is:
struct CParams {
i_insert_max: Option<i64>,
}
Then None means "no value provided" more explicitly than -1.
Comparisons
| Approach | When to use it | Pros | Cons |
|---|---|---|---|
new() method | You want a familiar constructor | Clear and explicit | Can become repetitive |
Manual Default implementation | You need custom defaults like -1 | Idiomatic, reusable, works well with struct update syntax | Slightly more code up front |
#[derive(Default)] | Type defaults are correct for every field | Very short | Cannot set custom defaults like -1 |
| Struct update syntax | You want to override a few fields | Concise and readable | Usually depends on Default or another existing struct |
new() vs Default
let a = CParams::new();
let b = CParams::default();
new()is common and easy to understand.Defaultis more idiomatic when a type has a natural default state.Defaultworks especially well with generic code and..Default::default().
Sentinel value vs Option
| Choice | Meaning | Good when | Drawback |
|---|---|---|---|
-1 | Special numeric marker | You must interact with legacy logic or fixed formats | Can be unclear |
Option<i64> | Some(value) or None | Absence of a value is meaningful | Slightly more handling required |
Cheat Sheet
Quick reference
Define a struct
struct CParams {
i_insert_max: i64,
t_continue: bool,
}
Constructor with new()
impl CParams {
fn new() -> Self {
Self {
i_insert_max: -1,
t_continue: false,
}
}
}
Implement Default
impl Default for CParams {
fn default() -> Self {
Self {
i_insert_max: -1,
t_continue: false,
}
}
}
Create with defaults
let p = CParams::default();
Override some fields
let p = CParams {
i_insert_max: 100,
..Default::default()
};
Auto-derive defaults
#[derive(Default)]
struct Flags {
enabled: bool,
retries: i32,
}
Important rules
- Rust does not support inline field default values in struct definitions.
- Use
Defaultwhen the type has a natural default state. - Use manual
impl Defaultfor custom values. - Use
#[derive(Default)]only when each field's type default is correct. - Use
..Default::default()to fill in omitted fields.
Edge case
If -1 means "unset", consider whether Option<i64> is clearer.
FAQ
Can I assign default values directly in a Rust struct definition?
No. Rust struct definitions only declare field names and types. Default values must be provided through construction logic such as new() or Default.
Is Default more idiomatic than new() in Rust?
Often, yes. If a type has a natural default state, implementing Default is idiomatic and works nicely with other Rust features.
When should I use #[derive(Default)]?
Use it when every field should use its type's built-in default value, such as 0 for integers and false for booleans.
Why doesn't #[derive(Default)] work for -1?
Because deriving Default uses the default of the field's type. For i64, that default is 0, not -1.
Can I keep both new() and Default?
Yes. A common pattern is for new() to simply call Self::default().
Is using -1 as a default a good idea?
It can work, but it may be less clear than using Option<i64> when you mean "no value set".
What is ..Default::default() doing?
It fills in all unspecified fields using the struct's Default implementation.
Mini Project
Description
Create a small Rust configuration type for a command-line import tool. The struct should have sensible default values, but allow a caller to override only the settings they care about. This demonstrates the practical use of Default and struct update syntax in a realistic setup scenario.
Goal
Build a Rust struct with custom default values and create multiple configurations by overriding selected fields.
Requirements
- Define a struct named
ImportConfigwith at least four fields. - Implement the
Defaulttrait manually because at least one field must use a non-standard default value. - Create one config using
ImportConfig::default(). - Create another config that overrides one or two fields using struct update syntax.
- Print both configurations to verify the values.
Keep learning
Related questions
Absolute Values in Rust: Using the abs() Method
Learn how to calculate absolute values in modern Rust with abs(), handle signed integer overflow, and choose safer alternatives.
Accepting Iterators in Rust Functions with impl Iterator
Learn how to write Rust functions that accept HashMap iterators using generic bounds or impl Iterator, avoiding trait object and lifetime errors.
Access Enum Variant Values in Rust with Pattern Matching
Learn how to extract and print values stored in Rust enum variants using match and if let pattern matching, with practical examples.