Rust doesn’t support default function arguments. Or does it?

A default argument example

I’ll pick Python as the language for the example because I think it has a pretty flexible function argument scheme (it allows for multiple combinations of default valued arguments with ordered or named arguments).

def my_func(req1, req2, opt1=123, opt2="abc", opt3=false):
print("req1 is {}".format(req1))
print("req2 is {}".format(req2))
print("opt1 is {}".format(opt1))
print("opt2 is {}".format(opt2))
print("opt3 is {}".format(opt3))
my_func("req1value", "req2value")
my_func("req1value", "req2value", 456)
my_func("req1value", "req2value", opt1=456, opt2="def", opt3=true)
my_func("req1value", "req2value", 456, "def", true)
my_func("req1value", "req2value", opt2="ghi")
my_func("req1value", "req2value", opt3=true, opt1=789)

The Default trait

In order to reproduce the example calls above, we’ll try to use the Default trait. So I’ll provide a short explanation of how it works.

struct MyStruct {
a: i32,
b: String,
c: bool,
}
impl Default for MyStruct {
fn default() -> Self {
MyStruct {
a: 123,
b: String::from("abc"),
c: false,
}
}
}
let my_instance = MyStruct {
a: 456,
..Default::default()
};
let my_instance2 = MyStruct {
a: 789,
c: true,
..Default::default()
};

Using the Default trait for default arguments

At this point, it’s probably not hard to guess how we’ll use the Default trait to emulate default arguments.

pub struct MyFuncOptionalArgs<'a> {
pub opt1: i32,
pub opt2: &'a str,
pub opt3: bool,
}
impl<'a> Default for MyFuncOptionalArgs<'a> {
fn default() -> Self {
MyFuncOptionalArgs {
opt1: 123,
opt2: "abc",
opt3: false,
}
}
}
pub fn my_func(
req1: &str,
req2: &str,
optional_args: MyFuncOptionalArgs
) {
println!("req1 is {}", req1);
println!("req2 is {}", req2);
println!("opt1 is {}", optional_args.opt1);
println!("opt2 is {}", optional_args.opt2);
println!("opt3 is {}", optional_args.opt3);
}
my_func("req1value", "req2value", MyFuncOptionalArgs::default());my_func(
"req1value",
"req2value",
MyFuncOptionalArgs {
opt1: 456,
opt2: "def",
opt3: true,
},
);
my_func(
"req1value",
"req2value",
MyFuncOptionalArgs {
opt2: "ghi",
..Default::default()
},
);
my_func(
"req1value",
"req2value",
MyFuncOptionalArgs {
opt3: true,
opt1: 789,
..Default::default()
},
);

Bonus: Making every argument a named argument

Named arguments can sometimes be great for making the code more readable. What if we wanted to also make the required arguments of our function behave like name arguments?

pub struct MyFuncArgs<'a> {
pub req1: &'a str,
pub req2: &'a str,
pub optional_args: MyFuncOptionalArgs<'a>,
}
pub struct MyFuncOptionalArgs<'a> {
pub opt1: i32,
pub opt2: &'a str,
pub opt3: bool,
}
impl<'a> Default for MyFuncOptionalArgs<'a> {
fn default() -> Self {
MyFuncOptionalArgs {
opt1: 123,
opt2: "abc",
opt3: false,
}
}
}
pub fn my_func(args: MyFuncArgs) {
println!("req1 is {}", args.req1);
println!("req2 is {}", args.req2);
println!("opt1 is {}", args.optional_args.opt1);
println!("opt2 is {}", args.optional_args.opt2);
println!("opt3 is {}", args.optional_args.opt3);
}
my_func(MyFuncArgs {
req1: "req1value",
req2: "req2value",
optional_args: MyFuncOptionalArgs::default(),
});
my_func(MyFuncArgs {
req2: "req2value",
optional_args: MyFuncOptionalArgs {
opt1: 456,
opt2: "def",
opt3: true,
},
req1: "req1value",
});
my_func(MyFuncArgs {
req1: "req1value",
req2: "req2value",
optional_args: MyFuncOptionalArgs {
opt2: "ghi",
..Default::default()
},
});
my_func(MyFuncArgs {
req1: "req1value",
req2: "req2value",
optional_args: MyFuncOptionalArgs {
opt3: true,
opt1: 789,
..Default::default()
},
});

An example in which this pattern was useful

This pattern was very useful in the function that draws sprites in my game (see the DrawImageArgs struct):

  • Drawing sprites is an ubiquitous thing in my game that gets called from many different places.
  • There are arguments that I consider required, such as the source image of the sprite, the position it will be drawn in the screen, the size it will be drawn and the depth relative to other draw calls.
  • There are also arguments that I consider optional which are specified in just a few cases and that have sensible default values for all the other calls that don’t care about them. For example, the color that should multiply the sprite (in most cases the sprite’s original colors should be used), the rotation to rotate the sprite (in most cases there’s no rotation), the opacity(alpha) of the draw, whether just a partial region of the source image should be used, and so on.
  • Since many of these arguments have the same types (position and size for example both have type F2, which in my game is used for 2-dimensional floating point vectors), having named arguments in the call sites is super helpful to clarify which is which and avoid confusion.

Is this a zero cost abstraction?

Unfortunately, it doesn’t seem completely free. I’ve inspected and compared the generated assembly from a default arguments function implementation against the corresponding explicit arguments implementation and noticed that, when I don’t allow the my_func to be inlined, the main+my_func assemblies have 25 additional instructions in the default implementation (131 vs 106 instructions in total). When inlining is allowed though, there was almost no difference: 285 vs 282 instructions in total (which seems very close to zero cost).

(default arguments) mean runtime: 59.268ms, std_dev: 3.343ms
(explicit arguments) mean runtime: 59.351ms, std_dev: 3.340ms
(default arguments) mean runtime: 883.652ms, std_dev: 31.416ms
(explicit arguments) mean runtime: 167.352ms, std_dev: 7.446ms
pub struct MyFuncOptionalArgs<'a> {
pub opt1: i32,
pub opt2: &'a str,
pub opt3: bool,
pub expensive_opt: Option<Vec<i32>>,
}
impl<'a> Default for MyFuncOptionalArgs<'a> {
fn default() -> Self {
MyFuncOptionalArgs {
opt1: 123,
opt2: "abc",
opt3: false,
expensive_opt: Some(expensive_vec_instantiation()),
}
}
}
...// This instantiation still calls expensive_vec_instantiation() even though it's
// not necessary.
let args = MyFuncOptionalArgs {
expensive_opt: None,
..Default::default()
};

Could it be zero cost?

To be honest, I’m currently not in a good position to tell. Although I don’t see any fundamental limitations preventing the compiler to optimize the struct arguments with partial defaults to the same level of performance of explicit arguments, I have to admit I’m not familiar at all with the internals of the Rust compiler yet to tell whether that would be possible or practical.

Final thoughts

You probably noticed the default arguments pattern presented in this post is somewhat verbose. For a single function it requires defining two additional structs and implementing the Default trait for one of them. I don't think that's a big deal though. I consider that default arguments should be used judiciously, so the price of having to add a few extra structs is probably a good incentive to make one think twice if it's really a good idea. If it is even then, it seems it's worth the price.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Luca Mattos Moller

Luca Mattos Moller

I'm a Computer Engineer interested in all sorts of stuff. Check out my personal website at lucamoller.com