0% found this document useful (0 votes)
20 views16 pages

TRPL Rantai Dev Docs Part II Chapter 14 ...

Chapter 14 of 'The Rust Programming Language' focuses on the importance of functions in Rust, detailing their declarations, definitions, and various attributes. It emphasizes the need for functions to enhance code clarity, maintainability, and efficiency by breaking down complex tasks into smaller, manageable pieces. The chapter also covers argument passing, return values, and best practices for function design, including the use of inline and constexpr functions.

Uploaded by

labviewerfani
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
20 views16 pages

TRPL Rantai Dev Docs Part II Chapter 14 ...

Chapter 14 of 'The Rust Programming Language' focuses on the importance of functions in Rust, detailing their declarations, definitions, and various attributes. It emphasizes the need for functions to enhance code clarity, maintainability, and efficiency by breaking down complex tasks into smaller, manageable pieces. The chapter also covers argument passing, return values, and best practices for function design, including the use of inline and constexpr functions.

Uploaded by

labviewerfani
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 16

menu search Search EN

Menu_Book The Rust Programming Home / Part II / Chapter 14


Language

Toc Table of Contents


article Chapter 14 ON THIS PAGE
14.1. Why Functions?
Functions 14.2. Function Declarations
Contact_mail Preface 14.3. Parts of a Function
Declaration
💡 "The real problem is that programmers have spent far too much time worrying about
send Foreword efficiency in the wrong places and at the wrong times; premature optimization is the root of 14.4. Function Definitions
all evil (or at least most of it) in programming." — Donald Knuth 14.5. Returning Values
article How to Use TRPL 14.6. inline Functions
14.7. constexpr Functions
flight_takeoff Part I - Introduction to
Rust 📘 Chapter 14 of TRPL - "Functions" delves into the critical role of function declarations in Rust, 14.8. Conditional Evaluation
discussing why functions are fundamental to programming and breaking down the various 14.9. [[noreturn]] Functions
Book Part I  parts of a function declaration. It covers the intricacies of function definitions, the mechanics
of returning values, and the specifics of inline and constexpr functions. The chapter also
14.10. Local Variables
14.11. Argument Passing
Foundation Part II - Basic Facilities explores special function attributes like \[\[noreturn\]\] and the handling of local variables. In
terms of argument passing, it addresses different methods including reference, array, list 14.12. Reference Arguments

Book Part II arguments, unspecified numbers of arguments, and default arguments. The section on 14.13. Array Arguments

overloaded functions explains automatic overload resolution, the impact of return types, scope 14.14. List Arguments
 Chapter 8 considerations, resolution strategies for multiple arguments, and techniques for manual 14.15. Unspecified Number of
overload resolution. Additionally, the chapter highlights the importance of preconditions and Arguments
 Chapter 9 postconditions, pointers to functions, and the use of macros, including conditional compilation, 14.16. Default Arguments
 Chapter 10 predefined macros, and pragmas. Practical advice is interspersed throughout, aimed at 14.17. Overloaded Functions
 Chapter 11 optimizing function use and ensuring robust and efficient code. 14.18. Automatic Overload
Resolution
 Chapter 12
 Chapter 13 14.1. Why Functions? 14.19. Overloading and
Return Type
 Chapter 14 In Rust, functions are key to creating clear, maintainable, and efficient code. They help in breaking 14.20. Overloading and
down complex problems into smaller, manageable pieces, enhancing both readability and Scope
 Chapter 15 14.21. Resolution for Multiple
maintainability.
 Chapter 16 Arguments
Consider a scenario where we need to perform a series of calculations. If we write all the logic in a 14.22. Manual Overload
Bolt Part III - Abstraction
Mechanism
single function, it might stretch to hundreds or even thousands of lines. Such lengthy functions can be
difficult to understand, maintain, and debug. The primary role of functions is to split these complex
Resolution
14.23. Pre- and
Book Part III  tasks into smaller, more manageable parts, each with a meaningful name. This practice not only makes
the code more understandable but also easier to maintain.
Postconditions
14.24. Pointer to Function
Code Part IV Common Libraries
For example, imagine we are implementing a system to process and analyze data. Instead of writing all
14.25. Macros
14.26. Conditional
Book Part IV  the data processing logic in one long function, we can break it down into smaller functions. Each
function performs a specific task, such as parsing data, filtering records, or computing statistics. By
Compilation
14.27. Predefined Macros
done Closing Remark giving these functions descriptive names, such as parse_data , filter_records , and
compute_statistics , we make the code more intuitive.
14.28. Pragmas
14.29. Advices
Rust's standard library offers functions like find and sort that serve as building blocks for more 14.30. Further Learning with
complex operations. We can leverage these built-in functions to handle common tasks and then GenAI
combine them to achieve more sophisticated results. This modular approach allows us to construct
complex operations from simple, well-defined functions.
When functions become excessively long, the likelihood of errors increases. Each additional line of
code introduces more potential for mistakes. By keeping functions short and focused on a single task,
we minimize the chance of errors. Short functions also force us to name and document their purpose
and dependencies, leading to more self-explanatory code.
In Rust, a good practice is to keep functions small enough to fit on a single screen—typically around
40 lines or fewer. However, for optimal clarity, aiming for functions that are around 7 lines is even
better. While function calls do have some overhead, it's generally negligible for most use cases. For
performance-critical functions, such as those accessed frequently, Rust's compiler can inline them to
reduce this overhead.
In addition to improving clarity, using functions helps avoid error-prone constructs like goto and
overly complex loops. Instead, Rust encourages the use of iterators and other straightforward
methods. For instance, nested loops can be replaced with iterators that handle complex operations
more safely and elegantly.
In summary, functions are essential for structuring code in Rust. By breaking down tasks into smaller
functions, we create a clear, maintainable codebase. This approach not only makes our code easier to
understand and debug but also helps in managing complexity and improving overall performance.

14.2. Function Declarations


Performing tasks primarily involves calling functions. A function must be declared before it can be
invoked, and its declaration outlines the function's name, the return type (if any), and the types and
number of arguments it accepts. Here are some examples:

1 fn next_elem() -> &Elem; // no arguments; returns a reference to Elem


2 fn exit(code: i32); // takes an i32 argument; returns nothing
3 fn sqrt(value: f64) -> f64; // takes an f64 argument; returns an f64

Argument passing semantics mirror those of copy initialization. Type checking and implicit type
conversions are applied as needed. For instance:

let s2 = sqrt(2.0); // calls sqrt() with an f64 argument of 2.0


let s3 = sqrt("three"); // error: sqrt() requires an f64 argument

This rigorous checking and conversion are essential for maintaining code safety and accuracy.
Function declarations often include argument names for clarity, although these names are not
mandatory for the function to compile if it's just a declaration. A return type of () signifies that the
function does not return a value.
A function's type comprises its return type and its argument types. For methods within structs or
enums, the struct or enum's name becomes part of the function type. For example:

fn f(i: i32, info: &Info) -> f64; // type: fn(i32, &Info) -> f64
fn String::index(&self, idx: usize) -> &char; // type: fn(&String, usize) -> &char

This setup ensures functions are well-defined and utilized properly, fostering the creation of robust
and efficient code.
14.3. Parts of a Function Declaration
A function declaration not only specifies the name, arguments, and return type but can also include
various specifiers and modifiers. Here are the components:
Name of the function: This is mandatory.
Argument list: This may be empty (); it is also mandatory.
Return type: This can be void and may be indicated as a prefix or suffix (using auto ); it is
required.
In addition, function declarations can include:
inline : Suggests that the function should be inlined for efficiency.

const : Indicates the function cannot modify the object it is called on.

static : Denotes a function not tied to a specific instance.

async : Marks the function as asynchronous.

unsafe : Indicates the function performs unsafe operations.

extern : Specifies that the function links to external code.

pub : Makes the function publicly accessible.

#[no_mangle] : Prevents the compiler from changing the function name during compilation.

#[inline(always)] : Instructs the compiler to always inline the function.

#[inline(never)] : Instructs the compiler to never inline the function.

Furthermore, functions can also be marked with attributes that specify their behavior, such as
indicating that the function will not return normally, often used for entry points in certain environments.
These elements collectively provide a comprehensive way to declare functions, ensuring their
purpose, usage, and behavior are clearly communicated and understood.

14.4. Function Definitions


Every callable function in a program must have a corresponding definition. A function definition
includes the actual code that performs the function's task, while a declaration specifies the function's
interface without its implementation.
For instance, to define a function that swaps two integers:

1 fn swap(x: &mut i32, y: &mut i32) {


2 let temp = *x;
3 *x = *y;
4 *y = temp;
5 }

The definition and all declarations of a function must maintain consistent types. Note that to ensure
compatibility, the const qualifier at the highest level of an argument type is ignored. For example,
these two declarations are considered the same:

fn example(x: i32);
fn example(x: i32);

Whether example() is defined with or without the const modifier does not change its type, and the
actual argument can be modified within the function based on its implementation.
Here's how example() can be defined:

1 fn example(x: i32) {
2 // the value of x can be modified here
3 }

Alternatively:

1 fn example(x: i32) {
2 // the value of x remains constant here
3 }

Argument names in function declarations are not part of the function type and can vary across
different declarations. For instance:

1 fn max(a: i32, b: i32, c: i32) -> i32 {


2 if a > b && a > c {
3 a
4 } else if b > c {
5 b
6 } else {
7 c
8 }
9 }

In declarations that are not definitions, naming arguments is optional and primarily used for
documentation. Conversely, if an argument is unused in a function definition, it can be left unnamed:

1 fn search(table: &Table, key: &str, _: &str) {


2 // the third argument is not used
3 }

Several constructs follow similar rules to functions, including:


Constructors: These initialize objects and do not return a value, adhering to specific initialization
rules.
Destructors: Used for cleanup when an object goes out of scope, they cannot be overloaded.
Function objects (closures): These implement the Fn trait but are not functions themselves.
Lambda expressions: These provide a concise way to create closures and can capture variables
from their surrounding scope.
14.5. Returning Values
In function declarations, it's crucial to specify the return type, with the exception of constructors and
type conversion functions. Traditionally, the return type appears before the function name, but modern
syntax allows placing the return type after the argument list. For example, the following declarations
are equivalent:

fn to_string(a: i32) -> String;


auto to_string(int a) -> string;

The prefix auto in the second example indicates that the return type follows the argument list,
marked by -> . This syntax is particularly useful in function templates where the return type depends
on the arguments. For instance:

template<class T, class U>


auto product(const vector<T>& x, const vector<U>& y) -> decltype(x*y);

This suffix return type syntax is similar to that used in lambda expressions, although they are not
identical. Functions that do not return a value are specified with a return type of void .
For non-void functions, a return value must be provided. Conversely, it is an error to return a value
from a void function. The return value is specified using a return statement:

1 fn fac(n: i32) -> i32 {


2 if n > 1 { n * fac(n - 1) } else { 1 }
3 }

A function that calls itself is considered recursive. Multiple return statements can be used within a
function:

1 fn fac2(n: i32) -> i32 {


2 if n > 1 {
3 return n * fac2(n - 1);
4 }
5 1
6 }

The semantics of returning a value are the same as those of copy initialization. The return expression
is checked against the return type, and necessary type conversions are performed.
Each function call creates new copies of its arguments and local variables. Therefore, returning a
pointer or reference to a local non-static variable is problematic because the variable's memory will be
reused after the function returns:

1 fn fp() -> &i32 {


2 let local = 1;
3 &local // this is problematic
4 }

Similarly, returning a reference to a local variable is also an error:

1 fn fr() -> &i32 {


2 let local = 1;
3 local // this is problematic
4 }

Compilers typically warn about such issues. Although there are no void values, a call to a void function
can be used as the return value of another void function:

1 fn g(p: &i32);
2 fn h(p: &i32) {
3 return g(p); // equivalent to g(p); return;
4 }

This form of return is useful in template functions where the return type is a template parameter. A
function can exit in several ways:
Executing a return statement.
Reaching the end of the function body in void functions and main(), indicating successful
completion.
Throwing an uncaught exception.
Terminating due to an uncaught exception in a noexcept function.
Invoking a system function that does not return (e.g., exit()).
Functions that do not return normally can be marked with [[noreturn]] .

14.6. inline Functions


Defining a function as inline suggests to the compiler that it should attempt to generate inline code at
each call site rather than using the standard function call mechanism. For example:

1 inline fn fac(n: i32) -> i32 {


2 if n < 2 { 1 } else { n * fac(n - 1) }
3 }

The inline specifier is a hint for the compiler to replace the function call with the function code itself,
which can optimize calls like fac(6) into a constant value such as 720. However, the extent to which
this inlining occurs depends on the compiler's optimization capabilities. Some compilers might
produce the constant 720, others might compute 6 * fac(5) recursively, and others might not inline
the function at all. For assured compile-time evaluation, declare the function as constexpr and
ensure all functions it calls are also constexpr .
To make inlining possible, the function definition must be available in the same scope as its
declaration. The inline specifier does not change the function's semantics; an inline function still has a
unique address, and so do any static variables within it.
If an inline function is defined in multiple translation units (for example, by including it in a header file
used in different source files), the definition must be identical in each translation unit to ensure
consistent behavior.

14.7. constexpr Functions


Generally, functions are not evaluated at compile time and thus cannot be used in constant
expressions. By marking a function as constexpr , you indicate that it can be evaluated at compile
time when given constant arguments. For instance:

1 const fn fac(n: i32) -> i32 {


2 if n > 1 { n * fac(n - 1) } else { 1 }
3 }
4
5 const F9: i32 = fac(9); // This must be evaluated at compile time

Using constexpr in a function definition means that the function can be used in constant expressions
with constant arguments. For an object, it means its initializer must be evaluated at compile time. For
example:

1 fn f(n: i32) {
2 let f5 = fac(5); // Might be evaluated at compile time
3 let fn_var = fac(n); // Evaluated at runtime since n is a variable
4 const F6: i32 = fac(6); // Must be evaluated at compile time
5 // const Fnn: i32 = fac(n); // Error: can't guarantee compile-time evaluation for n
6 let a: [u8; fac(4) as usize]; // OK: array bounds must be constants and fac() is constexpr
7 // let a2: [u8; fac(n) as usize]; // Error: array bounds must be constants, n is a variable
8 }

To be evaluated at compile time, a function must be simple: it should consist of a single return
statement, without loops or local variables, and it must not have side effects, meaning it should be a
pure function. For example:

1 let mut glob: i32 = 0;


2
3 const fn bad1(a: i32) {
4 // glob = a; // Error: side effect in constexpr function
5 }
6
7 const fn bad2(a: i32) -> i32 {
8 if a >= 0 { a } else { -a } // Error: if statement in constexpr function
9 }
10
11 const fn bad3(a: i32) -> i32 {
12 let mut sum = 0;
13 // for i in 0..a { sum += fac(i); } // Error: loop in constexpr function
14 sum
15 }

The rules for a constexpr constructor are different, allowing only simple member initialization.
A constexpr function supports recursion and conditional expressions, enabling a broad range of
operations. However, overusing constexpr for complex tasks can complicate debugging and
increase compile times. It is best to reserve constexpr functions for simpler tasks they are intended
for.
Using literal types, constexpr functions can work with user-defined types. Similar to inline functions,
constexpr functions follow the one-definition rule (ODR), requiring identical definitions in different
translation units. Think of constexpr functions as a more restricted form of inline functions.

14.8. Conditional Evaluation


In a constexpr function, branches of conditional expressions that are not executed are not evaluated.
This allows a branch that isn't taken to still require run-time evaluation. For example:

1 const fn check(i: i32) -> i32 {


2 if LOW <= i && i < HIGH {
3 i
4 } else {
5 panic!("out_of_range");
6 }
7 }
8
9 const LOW: i32 = 0;
10 const HIGH: i32 = 99;
11
12 // ...
13
14 const VAL: i32 = check(f(x, y, z));

Here, LOW and HIGH can be considered configuration parameters that are known at compile time, but
not at design time. The function f(x, y, z) computes a value based on implementation specifics.
This example illustrates how conditional evaluation in constexpr functions can handle compile-time
constants while permitting run-time calculations when needed.

14.9. [[noreturn]] Functions


The construct #[...] is referred to as an attribute and can be used in various parts of Rust's syntax.
Attributes generally specify some implementation-specific property about the syntax element that
follows them. One such attribute is #[noreturn] .
When you place #[noreturn] at the beginning of a function declaration, it indicates that the function
is not supposed to return. For example:

1 #[noreturn]
2 fn exit(code: i32) -> ! {
3 // implementation that never returns
4 }

Knowing that a function does not return helps in understanding the code and can assist in optimizing
it. However, if a function marked with #[noreturn] does return, the behavior is undefined.

14.10. Local Variables


In a function, names defined are generally referred to as local names. When a local variable or
constant is initialized, it occurs when the execution thread reaches its definition. If not declared as
static , each call to the function creates its own instance of the variable. On the other hand, if a local
variable is declared static , a single statically allocated object is used for that variable across all
function calls, initializing only the first time the execution thread encounters it.
For example:

1 fn f(mut a: i32) {
2 while a > 0 {
3 static mut N: i32 = 0; // Initialized once
4 let x: i32 = 0; // Initialized on each call of f()
5
6 unsafe {
7 println!("n == {}, x == {}", N, x);
8 N += 1;
9 }
10 a -= 1;
11 }
12 }
13
14 fn main() {
15 f(3);
16 }

This code will output:

1 n == 0, x == 0
2 n == 1, x == 0
3 n == 2, x == 0

The use of a static local variable enables a function to retain information between calls without
needing a global variable that could be accessed or altered by other functions. The initialization of a
static local variable does not cause a data race unless the function containing it is entered recursively
or a deadlock occurs. Rust handles this by protecting the initialization of a local static variable with
constructs like std::sync::Once . However, recursively initializing a static local variable leads to
undefined behavior.
Static local variables help avoid dependencies among nonlocal variables. If you need a local function,
consider using a closure or a function object instead. In Rust, the scope of a label spans the entire
function, regardless of the nested scope where it might be located.

14.11. Argument Passing


When a function is called using the call operator () , memory is allocated for its parameters, and each
parameter is initialized with its corresponding argument. This process follows the same rules as copy
initialization, meaning the types of the arguments are checked against the types of the parameters,
and necessary conversions are performed. If a parameter is not a reference, a copy of the argument is
passed to the function.
For instance, consider a function that searches for a value in an array slice:

1 fn find(slice: &[i32], value: i32) -> Option<usize> {


2 for (index, &element) in slice.iter().enumerate() {
3 if element == value {
4 return Some(index);
5 }
6 }
7 None
8 }
9
10 fn g(slice: &[i32]) {
11 if let Some(index) = find(slice, 'x' as i32) {
12 // ...
13 }
14 }

In this example, the original slice passed to the find function within g remains unmodified since
slices are passed by reference in Rust.
Rust has particular rules for passing arrays and allows for unchecked arguments through the use of
unsafe blocks. Default arguments can be handled using function overloading or optional parameters
with Option . Initializer lists are supported via macros or custom initializers, and argument passing in
generic functions is handled in a type-safe manner.
This approach ensures that arguments are passed efficiently and safely, adhering to Rust’s principles
of ownership and borrowing.

14.12. Reference Arguments


Understanding how to pass arguments to functions is essential, particularly the distinction between
passing by value and passing by reference. When a function takes an argument by value, it creates a
copy of the original data, which means changes within the function do not affect the original variable.
Conversely, passing by reference allows the function to modify the original variable.
Consider a function f that takes two parameters: an integer by value and another integer by
reference. Incrementing the value parameter only changes the local copy within the function, while
incrementing the reference parameter modifies the actual argument passed to the function.

1 fn f(mut val: i32, ref: &mut i32) {


2 val += 1;
3 *ref += 1;
4 }

If we invoke this function with two integers, the first integer remains unchanged outside the function,
while the second integer is incremented.

1 fn g() {
2 let mut i = 1;
3 let mut j = 1;
4 f(i, &mut j);
5 }

Here, i stays 1 after the function call, whereas j becomes 2 . Using functions that modify call-by-
reference arguments can make programs harder to read and should generally be avoided unless
necessary. However, passing large objects by reference can be more efficient than passing by value.
In such scenarios, declaring the parameter as a const reference ensures the function does not
modify the object.

1 fn f(arg: &Large) {
2 // arg cannot be modified
3 }
When a reference parameter is not marked as const , it implies an intention to modify the variable.

1 fn g(arg: &mut Large) {


2 // assume g modifies arg
3 }

Similarly, declaring pointer parameters as const indicates the function will not alter the object being
pointed to.

1 fn strlen(s: &str) -> usize {


2 // returns the length of the string
3 }

Using const increases code clarity and reduces potential errors, especially in larger programs. The
rules for reference initialization allow literals, constants, and arguments requiring conversion to be
passed as const T& but not as non-const T& . This ensures safe and efficient passing of arguments
without unintended temporaries.

1 fn update(i: &mut f32) {


2 // updates the value of i
3 }

Passing arguments by rvalue references enables functions to modify temporary objects or objects
about to be destroyed, which is useful for implementing move semantics.

1 fn f(v: Vec<i32>) {
2 // takes ownership of v
3 }
4
5 fn g(vi: &mut Vec<i32>, cvi: &Vec<i32>) {
6 f(vi.clone());
7 f(cvi.clone());
8 f(vec![1, 2, 3, 4]);
9 }

In this example, the function f can accept vectors and modify them, making it suitable for move
semantics. Generally, rvalue references are used for defining move constructors and move
assignments.
When deciding how to pass arguments, consider these guidelines:
1 Use pass-by-value for small objects.
2 Use pass-by-const-reference for large objects that don't need modification.
3 Return results directly instead of modifying arguments.
4 Use rvalue references for move semantics and forwarding.
5 Use pointers if "no object" is a valid option (represented by Option ).
6 Use pass-by-reference only when necessary.
Following these guidelines ensures efficient and clear argument passing, maintaining the principles of
ownership and borrowing.

14.13. Array Arguments


When an array is used as a function argument, a pointer to its first element is passed. For example:

1 fn strlen(s: &str) -> usize {


2 s.len()
3 }
4
5 fn f() {
6 let v = "Annemarie";
7 let i = strlen(v);
8 let j = strlen("Nicholas");
9 }

This means that an argument of type T[] will be converted to T* when passed to a function, allowing
modifications to array elements within the function. Unlike other types, arrays are passed by pointer
rather than by value.
A parameter of array type is equivalent to a parameter of pointer type. For instance:

fn process_array(p: &mut [i32]) {}


fn process_array_ref(buf: &mut [i32]) {}

Both declarations are equivalent and represent the same function. The names of the arguments do not
affect the function's type. The rules for passing multidimensional arrays are similar.
The size of an array is not inherently available to the called function, which can lead to errors. One
solution is to pass the size of the array as an additional parameter:

1 fn compute1(vec: &[i32]) {
2 let vec_size = vec.len();
3 // computation
4 }

However, it is often better to pass a reference to a container like a vector or an array for more safety
and flexibility. For example:

1 fn process_fixed_array(arr: &[i32; 4]) {


2 // use array
3 }
4
5 fn example_usage() {
6 let a1 = [1, 2, 3, 4];
7 let a2 = [1, 2];
8 process_fixed_array(&a1); // OK
9 // process_fixed_array(&a2); // error: wrong number of elements
10 }

The number of elements is part of the reference-to-array type, making it less flexible than pointers or
containers. References to arrays are especially useful in templates where the number of elements can
be deduced:

1 fn process_generic_array<T, const N: usize>(arr: &[T; N]) {


2 // use array
3 }
4
5 fn example_generic_usage() {
6 let a1 = [1; 10];
7 let a2 = [2.0; 100];
8 process_generic_array(&a1); // T is i32, N is 10
9 process_generic_array(&a2); // T is f64, N is 100
10 }

This method generates as many function definitions as there are calls with distinct array types. For
multidimensional arrays, using arrays of pointers can often avoid special treatment:

let days: [&str; 7] = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];

Generally, using vectors and similar types is a better alternative to low-level arrays and pointers,
providing safer and more readable code.

14.14. List Arguments


A list enclosed in {} can be used as an argument for:
1 A parameter of type std::initializer_list , where the elements can be implicitly
converted to type T .
2 A type that can be initialized with the values provided in the list.
3 A reference to an array of T , where the elements can be implicitly converted to T .
Technically, the first case covers all scenarios, but it’s often clearer to consider each separately. For
instance:

1 fn f1<T>(list: &[T]) {}
2 struct S {
3 a: i32,
4 s: String,
5 }
6 fn f2(s: S) {}
7 fn f3<T, const N: usize>(arr: &[T; N]) {}
8 fn f4(n: i32) {}
9
10 fn g() {
11 f1(&[1, 2, 3, 4]); // T is i32 and the list has 4 elements
12 f2(S { a: 1, s: "MKS".to_string() }); // f2(S { a: 1, s: "MKS".to_string() })
13 f3(&[1, 2, 3, 4]); // T is i32 and N is 4
14 f4(1); // f4(i32::from(1))
15 }

In cases where there might be ambiguity, a parameter with initializer_list takes precedence.
For example:

1 fn f<T>(list: &[T]) {}
2 struct S {
3 a: i32,
4 s: String,
5 }
6 fn f(s: S) {}
7 fn f<T, const N: usize>(arr: &[T; N]) {}
8 fn f(n: i32) {}
9
10 fn g() {
11 f(&[1, 2, 3, 4]); // T is i32 and the list has 4 elements
12 f(S { a: 1, s: "MKS".to_string() }); // calls f(S)
13 f(&[1]); // T is i32 and the list has 1 element
14 }

The reason an initializer_list parameter is given priority is to avoid confusion if different


functions were selected based on the number of elements in the list. While it’s impossible to eliminate
all forms of confusion in overload resolution, prioritizing initializer_list parameters for {}-list
arguments helps reduce it.
If a function with an initializer_list parameter is in scope, but the list argument doesn't match,
another function can be chosen. The call f({1, "MKS"}) is an example of this. Note that these rules
specifically apply to std::initializer_list arguments. There are no special rules for
std::initializer_list& or for other types named initializer_list in different scopes.

14.15. Unspecified Number of Arguments


There are situations where specifying the number and type of all function arguments isn't feasible. In
such cases, you have three main options:
Variadic Templates: This method allows you to manage an arbitrary number of arguments of
different types in a type-safe manner. By using a template metaprogram, you can interpret the
argument list and perform the necessary actions.
Initializer Lists: Using std::initializer_list as the argument type lets you handle an
arbitrary number of arguments of a single type safely. This is particularly useful for homogeneous
lists, which are common in many contexts.
Ellipsis ( ... ): Terminating the argument list with ellipsis allows handling an arbitrary number of
arguments of almost any type using macros from . While this approach is not inherently type-safe
and can be cumbersome with complex user-defined types, it has been in use since the early days
of C.
The first two methods are described elsewhere. Here, we'll focus on the third method, despite its
limitations in most scenarios. For instance, consider a function declaration like this:

1 fn printf(format: &str, args: ...) -> i32 {


2 // Implementation details
3 }

This declaration specifies that a call to printf must have at least one argument (a format string), but
may include additional arguments. Examples of its usage include:
1 printf("Hello, world!\n");
2 printf("My name is %s %s\n", first_name, second_name);
3 printf("%d + %d = %d\n", 2, 3, 5);

Functions using unspecified arguments must rely on additional information (like a format string) to
interpret the argument list correctly. However, this approach often bypasses the compiler's ability to
check argument types and counts, leading to potential errors. For example:

std::printf("My name is %s %s\n", 2);

Although invalid, this code may not be flagged by the compiler, resulting in unpredictable behavior.
In scenarios where argument types and numbers can't be entirely specified, a well-designed program
might only need a few such functions. Alternatives like overloaded functions, default arguments,
initializer_list arguments, and variadic templates should be used whenever possible to
maintain type safety and clarity.
For instance, the traditional printf function from the C library:

int printf(const char* format, ...);

To handle variadic arguments, you might use a combination of va_list , va_start , va_arg , and
va_end macros from . Alternatively, a safer and more modern approach involves
std::initializer_list :

1 fn error(severity: i32, args: std::initializer_list<&str>) {


2 for arg in args {
3 eprintln!("{}", arg);
4 }
5 if severity > 0 {
6 std::process::exit(severity);
7 }
8 }

You could then call this function with a list of string arguments:

error(1, {"Error:", "Invalid input", "Please try again"});

Integrating these concepts in a modern programming context helps maintain type safety and
readability while managing varying numbers of arguments effectively. This aligns with best practices in
software development, ensuring that your programs are robust and maintainable.

14.16. Default Arguments


In many cases, functions require multiple parameters to handle complex scenarios effectively,
especially constructors that offer various ways to create objects. Consider a Complex class:

1 struct Complex {
2 re: f64,
3 im: f64,
4 }
5
6 impl Complex {
7 fn new(r: f64, i: f64) -> Complex {
8 Complex { re: r, im: i }
9 }
10
11 fn from_real(r: f64) -> Complex {
12 Complex { re: r, im: 0.0 }
13 }
14
15 fn default() -> Complex {
16 Complex { re: 0.0, im: 0.0 }
17 }
18 }

While the actions of these constructors are straightforward, having multiple functions performing
similar tasks can lead to redundancy. This redundancy is more apparent when constructors involve
complex logic. To reduce repetition, one constructor can call another:

1 impl Complex {
2 fn new(r: f64, i: f64) -> Complex {
3 Complex { re: r, im: i }
4 }
5
6 fn from_real(r: f64) -> Complex {
7 Complex::new(r, 0.0)
8 }
9
10 fn default() -> Complex {
11 Complex::new(0.0, 0.0)
12 }
13 }

This consolidation allows for easier implementation of additional functionalities, such as debugging or
logging, in a single place. Further simplification can be achieved with default arguments:

1 impl Complex {
2 fn new(r: f64 = 0.0, i: f64 = 0.0) -> Complex {
3 Complex { re: r, im: i }
4 }
5 }

With default arguments, if fewer arguments are provided, default values are automatically used. This
method clarifies that fewer arguments can be supplied, and defaults will be used as needed, making
the constructor's intent explicit and reducing redundancy.
A default argument is type-checked when the function is declared and evaluated when the function is
called. For example:

1 struct X {
2 def_arg: i32,
3 }
4
5 impl X {
6 fn new() -> X {
7 X { def_arg: 7 }
8 }
9
10 fn f(&self, arg: i32 = self.def_arg) {
11 // Function implementation
12 }
13 }
14
15 fn main() {
16 let mut a = X::new();
17 a.f(); // Uses default argument 7
18 a.def_arg = 9;
19 a.f(); // Uses updated default argument 9
20 }

However, changing default arguments can introduce subtle dependencies and should generally be
avoided. Default arguments can only be provided for trailing parameters. For example:

fn f(a: i32, b: i32 = 0, c: Option<&str> = None) { /*...*/ } // OK


fn g(a: i32 = 0, b: i32, c: Option<&str>) { /*...*/ } // Error

Reusing or altering a default argument in subsequent declarations in the same scope is not allowed:

1 fn f(x: i32 = 7); // Initial declaration


2 fn f(x: i32 = 7); // Error: cannot repeat default argument
3 fn f(x: i32 = 8); // Error: different default arguments
4
5 fn main() {
6 fn f(x: i32 = 9); // OK: hides outer declaration
7 // ...
8 }

Hiding a declaration with a nested scope can lead to errors and should be handled cautiously.
By utilizing default arguments appropriately, you can simplify function declarations, reduce
redundancy, and make your code more maintainable and understandable.

14.17. Overloaded Functions


While it's often recommended to give distinct names to different functions, there are situations where it
makes sense to use the same name for functions that perform similar tasks on different types. This
practice is known as overloading. For example, the addition operator (+) is used for both integers and
floating-point numbers. This concept can be extended to user-defined functions, allowing the same
name to be reused for different parameter types. For example:

1 fn print(x: i32) {
2 println!("{}", x);
3 }
4
5 fn print(s: &str) {
6 println!("{}", s);
7 }

In the compiler's view, overloaded functions share only their name; they may perform entirely different
tasks. The language does not enforce any similarity between them, leaving it up to the programmer.
Using overloaded function names can make code more intuitive, especially for commonly used
operations like print , sqrt , and open .
This approach is essential when the function name holds significant meaning, such as with operators
like + , * , and << , or with constructors in generic programming. Rust's traits and generics provide a
structured way to implement overloaded functions, enabling the use of the same function name with
different types safely and efficiently.

14.18. Automatic Overload Resolution


When a function called fct is invoked, the compiler needs to determine which specific version of
fct to execute by comparing the types of the actual arguments with the types of the parameters for
all functions named fct in scope. The goal is to match the arguments to the parameters of the best-
fitting function and produce a compile-time error if no suitable function is found. For example:

1 fn print(x: f64) {
2 println!("{}", x);
3 }
4
5 fn print(x: i64) {
6 println!("{}", x);
7 }
8
9 fn f() {
10 print(1i64); // Calls print(i64)
11 print(1.0); // Calls print(f64)
12 // print(1); // Error: ambiguous, could match print(i64) or print(f64)
13 }

To decide which function to call, the compiler uses a hierarchy of criteria:


Exact match, using no or only trivial conversions (e.g., reference adjustments or dereferencing).
Match using promotions, such as converting smaller integer types to larger ones or promoting
f32 to f64 .

Match using standard conversions (e.g., i32 to f64 , f64 to i32 , pointer conversions).
Match using user-defined conversions.
Match using the ellipsis ( ... ) in a function declaration.
If there are two matches at the highest level of criteria, the call is considered ambiguous and results in
a compile-time error. These detailed resolution rules primarily handle the complexity of numeric type
conversions.
For instance:

1 fn print(x: i32) {
2 println!("{}", x);
3 }
4
5 fn print(x: &str) {
6 println!("{}", x);
7 }
8
9 fn print(x: f64) {
10 println!("{}", x);
11 }
12
13 fn print(x: i64) {
14 println!("{}", x);
15 }
16
17 fn print(x: char) {
18 println!("{}", x);
19 }
20
21 fn h(c: char, i: i32, s: i16, f: f32) {
22 print(c); // Calls print(char)
23 print(i); // Calls print(i32)
24 print(s); // Promotes s to i32 and calls print(i32)
25 print(f); // Promotes f to f64 and calls print(f64)
26 print('a'); // Calls print(char)
27 print(49); // Calls print(i32)
28 print(0); // Calls print(i32)
29 print("a"); // Calls print(&str)
30 }

The call to print(0) selects print(i32) because 0 is an integer literal. Similarly, print('a')
calls print(char) since 'a' is a character. These rules prioritize safe promotions over potentially
unsafe conversions.
Overload resolution in Rust is independent of the order in which functions are declared. Function
templates are resolved by applying overload resolution rules after specializing the templates based on
provided arguments. Special rules also exist for overloading with initializer lists and rvalue references.
Although overloading relies on a complex set of rules, which can sometimes lead to unexpected
function calls, it simplifies the programmer's job by allowing the same function name to be used for
different types. Without overloading, we would need multiple function names for similar operations on
different types, leading to tedious and error-prone code. For example, without overloading, one might
need:

1 fn print_int(x: i32) {
2 println!("{}", x);
3 }
4
5 fn print_char(x: char) {
6 println!("{}", x);
7 }
8
9 fn print_str(x: &str) {
10 println!("{}", x);
11 }
12
13 fn g(i: i32, c: char, p: &str, d: f64) {
14 print_int(i); // OK
15 print_char(c); // OK
16 print_str(p); // OK
17 print_int(c as i32); // OK, but may print unexpected number
18 print_char(i as char); // OK, but may narrow unexpectedly
19 // print_str(i); // Error
20 print_int(d as i32); // OK, but narrowing conversion
21 }

Without overloading, the programmer has to remember multiple function names and use them
correctly, which can be cumbersome and prone to errors. Overloading allows the same function name
to be used for different types, increasing the chances that unsuitable arguments will be caught by the
compiler and reducing the likelihood of type-related errors.

14.19. Overloading and Return Type


Return types are not factored into function overload resolution. This design choice ensures that
determining which function to call remains straightforward and context-independent. For example:

1 fn sqrt(x: f32) -> f32 {


2 // implementation for f32
3 }
4
5 fn sqrt(x: f64) -> f64 {
6 // implementation for f64
7 }
8
9 fn f(da: f64, fla: f32) {
10 let fl: f32 = sqrt(da); // calls sqrt(f64)
11 let d: f64 = sqrt(da); // calls sqrt(f64)
12 let fl = sqrt(fla); // calls sqrt(f32)
13 let d = sqrt(fla); // calls sqrt(f32)
14 }

If the return type were considered during overload resolution, it would no longer be possible to look at
a function call in isolation to determine which function is being invoked. This would complicate the
resolution process, requiring the context of each call to identify the correct function. By excluding
return types from overload resolution, the language ensures that each function call can be resolved
based solely on its arguments, maintaining clarity and simplicity.

14.20. Overloading and Scope


Overloading occurs within the same scope, meaning functions declared in different, non-namespace
scopes do not participate in overloading together. For example:

1 fn f(x: i32) {
2 // implementation for i32
3 }
4
5 fn g() {
6 fn f(x: f64) {
7 // implementation for f64
8 }
9 f(1); // calls f(f64)
10 }

Here, although f(i32) would be a better match for f(1) , only f(f64) is in scope. Local
declarations can be adjusted to achieve the desired behavior. Intentional hiding can be useful, but
unintentional hiding can lead to unexpected results.
For base and derived classes, different scopes prevent overloading between base and derived class
functions by default:

1 struct Base {
2 fn f(&self, x: i32) {
3 // implementation for Base
4 }
5 }
6
7 struct Derived : Base {
8 fn f(&self, x: f64) {
9 // implementation for Derived
10 }
11 }
12
13 fn g(d: &Derived) {
14 d.f(1); // calls Derived::f(f64)
15 }

When overloading across class or namespace scopes is necessary, use declarations or directives can
be employed. Additionally, argument-dependent lookup can facilitate overloading across namespaces.
This ensures that overloading remains controlled and predictable, improving code readability and
maintainability.

14.21. Resolution for Multiple Arguments


Overload resolution rules are utilized to select the most appropriate function when multiple arguments
are involved, aiming for efficiency and precision across different data types. Here's an example:

1 fn pow(base: i32, exp: i32) -> i32 {


2 // implementation for integers
3 }
4
5 fn pow(base: f64, exp: f64) -> f64 {
6 // implementation for floating-point numbers
7 }
8
9 fn pow(base: f64, exp: Complex) -> Complex {
10 // implementation for double and complex numbers
11 }
12
13 fn pow(base: Complex, exp: i32) -> Complex {
14 // implementation for complex and integer
15 }
16
17 fn pow(base: Complex, exp: Complex) -> Complex {
18 // implementation for complex numbers
19 }
20
21 fn k(z: Complex) {
22 let i = pow(2, 2); // calls pow(i32, i32)
23 let d = pow(2.0, 2.0); // calls pow(f64, f64)
24 let z2 = pow(2.0, z); // calls pow(f64, Complex)
25 let z3 = pow(z, 2); // calls pow(Complex, i32)
26 let z4 = pow(z, z); // calls pow(Complex, Complex)
27 }

When the compiler selects the best match among overloaded functions with multiple arguments, it
finds the optimal match for each argument. A function is chosen if it is the best match for at least one
argument and an equal or better match for all other arguments. If no such function exists, the call is
considered ambiguous:

1 fn g() {
2 let d = pow(2.0, 2); // error: pow(i32::from(2.0), 2) or pow(2.0, f64::from(2))?
3 }

In this case, the call is ambiguous because 2.0 is the best match for the first argument of pow(f64,
f64) and 2 is the best match for the second argument of pow(i32, i32) .

14.22. Manual Overload Resolution


When functions are overloaded either too little or too much, it can lead to ambiguities. For example:

1 fn f1(ch: char) {
2 // implementation for char
3 }
4
5 fn f1(num: i64) {
6 // implementation for i64
7 }
8
9 fn f2(ptr: &mut char) {
10 // implementation for char pointer
11 }
12
13 fn f2(ptr: &mut i32) {
14 // implementation for int pointer
15 }
16
17 fn k(i: i32) {
18 f1(i); // ambiguous: f1(char) or f1(i64)?
19 f2(0 as *mut i32); // ambiguous: f2(&mut char) or f2(&mut i32)?
20 }

To avoid these issues, consider the entire set of overloaded functions to ensure they make sense
together. Often, adding a specific version can resolve ambiguities. For example, adding:

1 fn f1(n: i32) {
2 f1(n as i64);
3 }

This resolves ambiguities like f1(i) in favor of the larger type i64 .
Explicit type conversion can also address specific calls:

f2(0 as *mut i32);

However, this approach is often just a temporary fix and doesn't solve the core issue. Similar calls
might appear and need to be handled.
While beginners might find ambiguity errors frustrating, experienced programmers see these errors as
helpful indicators of potential design flaws.

14.23. Pre- and Postconditions


Functions come with expectations regarding their arguments. Some of these expectations are defined
by the argument types, while others depend on the actual values and relationships between them.
Although the compiler and linker can ensure type correctness, managing invalid argument values falls
to the programmer. Preconditions are the logical criteria that should be true when a function is called,
while postconditions are criteria that should be true when a function returns.
For example, consider a function that calculates the area of a rectangle:

1 fn area(len: i32, wid: i32) -> i32 {


2 // Preconditions: len and wid must be positive
3 // Postconditions: the return value must be positive and represent the area of the rectangle
4 len * wid
5 }

Documenting preconditions and postconditions like this is beneficial. It helps the function’s
implementer, users, and testers understand the function's requirements and guarantees. For instance,
values like 0 and -12 are invalid arguments. Moreover, if very large values are passed, the result might
overflow, violating the postconditions.
Consider calling area(i32::MAX, 2) :
Should the caller avoid such calls? Ideally, yes, but mistakes happen.
Should the implementer handle these cases? If so, how should errors be managed?
Various approaches exist. It’s easy for callers to overlook preconditions, and it’s challenging for
implementers to check all preconditions efficiently. While reliance on the caller is preferred, a
mechanism to ensure correctness is necessary. Some pre- and postconditions are easy to verify (e.g.,
len is positive), while others, like verifying "the return value is the area of a rectangle with sides len
and wid ," are more complex and semantic.
Writing out pre- and postconditions can reveal subtle issues in a function. This practice not only aids in
design and documentation but also helps in identifying potential problems early.
For functions that depend solely on their arguments, preconditions apply only to those arguments.
However, for functions relying on non-local values (e.g., member functions dependent on an object's
state), these values must be considered as implicit arguments. Similarly, postconditions for side-
effect-free functions ensure the value is correctly computed. If a function modifies non-local objects,
these effects must also be documented.
Function developers have several options:
Ensure every input results in a valid output, eliminating preconditions.
Assume the caller ensures preconditions are met.
Check preconditions and throw an exception if they fail.
Check preconditions and terminate the program if they fail.
If a postcondition fails, it indicates either an unchecked precondition or a programming error.

14.24. Pointer to Function


Just as data objects have memory addresses, the code for a function is stored in memory and can be
referenced through an address. We can use pointers to functions similarly to how we use pointers to
objects, but function pointers are limited to calling the function and taking its address.
To declare a pointer to a function, you specify it similarly to the function's own declaration. For
example:

1 fn error(s: String) { /* ... */ }


2 let mut efct: fn(String) = error;
3 efct("error".to_string());

In this example, efct is a function pointer that points to the error function and can be used to call
error .

Function pointers must match the complete function type, including argument types and return type,
exactly. Consider the following example:

1 fn f1(s: String) {}
2 fn f2(s: String) -> i32 { 0 }
3 fn f3(p: &i32) {}
4
5 let mut pf: fn(String);
6
7 pf = f1; // OK
8 pf = f2; // error: mismatched return type
9 pf = f3; // error: mismatched argument type

Casting function pointers to different types is possible but should be done with caution as it can lead
to undefined behavior. For instance:

1 type P1 = fn(&i32) -> i32;


2 type P2 = fn();
3
4 fn f(pf: P1) {
5 let pf2 = pf as P2;
6 pf2(); // likely causes a serious problem
7 let pf1 = pf2 as P1;
8 let x = 7;
9 let y = pf1(&x);
10 }

This example highlights the risks of casting function pointers and underscores the importance of
careful type management.
Function pointers are useful for parameterizing algorithms, especially when the algorithm needs to be
provided with different operations. For example, a sorting function might accept a comparison function
as an argument.
To sort a collection of User structs, you could define comparison functions and pass them to the sort
function:

1 struct User {
2 name: &'static str,
3 id: &'static str,
4 dept: i32,
5 }
6
7 fn cmp_name(a: &User, b: &User) -> std::cmp::Ordering {
8 a.name.cmp(b.name)
9 }
10
11 fn cmp_dept(a: &User, b: &User) -> std::cmp::Ordering {
12 a.dept.cmp(&b.dept)
13 }
14
15 fn main() {
16 let mut users = vec![
17 User { name: "Ritchie D.M.", id: "dmr", dept: 11271 },
18 User { name: "Sethi R.", id: "ravi", dept: 11272 },
19 User { name: "Szymanski T.G.", id: "tgs", dept: 11273 },
20 User { name: "Schryer N.L.", id: "nls", dept: 11274 },
21 User { name: "Schryer N.L.", id: "nls", dept: 11275 },
22 User { name: "Kernighan B.W.", id: "bwk", dept: 11276 },
23 ];
24
25 users.sort_by(cmp_name);
26 println!("Sorted by name:");
27 for user in &users {
28 println!("{} {} {}", user.name, user.id, user.dept);
29 }
30
31 users.sort_by(cmp_dept);
32 println!("\nSorted by department:");
33 for user in &users {
34 println!("{} {} {}", user.name, user.id, user.dept);
35 }
36 }

In this example, the vector of User structs is sorted first by name and then by department using
function pointers.
Pointers to functions provide a flexible way to parameterize algorithms and manage different
operations in a type-safe manner. However, it's crucial to ensure type compatibility to avoid errors and
undefined behavior.

14.25. Macros
Macros play a crucial role in C but are less prevalent in more modern languages like Rust. The key
guideline for using macros is to avoid them unless absolutely necessary. Most macros indicate a
limitation in the programming language, the program, or the programmer. Since macros manipulate
code before the compiler processes it, they can complicate tools like debuggers, cross-referencing,
and profilers. When macros are essential, it's important to read the reference manual for your
implementation of the Rust preprocessor and avoid overly clever solutions. Conventionally, macros are
named with capital letters to signal their presence.
In Rust, macros should primarily be used for conditional compilation and include guards. Here’s an
example of a simple macro definition:

1 macro_rules! NAME {
2 () => {
3 rest_of_line
4 };
5 }

When NAME is encountered as a token, it is replaced by rest_of_line .


Macros can also take arguments:

1 macro_rules! MAC {
2 ($x:expr, $y:expr) => {
3 println!("argument1: {}, argument2: {}", $x, $y);
4 };
5 }

Using MAC!(foo, bar) will expand to:

println!("argument1: foo, argument2: bar");

Macro names cannot be overloaded, and the macro preprocessor cannot handle recursive calls:

1 macro_rules! PRINT {
2 ($a:expr, $b:expr) => {
3 println!("{} {}", $a, $b);
4 };
5 ($a:expr, $b:expr, $c:expr) => {
6 println!("{} {} {}", $a, $b, $c);
7 };
8 }
9
10 macro_rules! FAC {
11 ($n:expr) => {
12 if $n > 1 {
13 $n * FAC!($n - 1)
14 } else {
15 1
16 }
17 };
18 }

Macros manipulate token streams and have a limited understanding of Rust syntax and types. Errors in
macros are detected when expanded, not when defined, leading to obscure error messages. Here are
some plausible macros:

1 macro_rules! CASE {
2 () => {
3 break; case
4 };
5 }
6 macro_rules! FOREVER {
7 () => {
8 loop {}
9 };
10 }

Avoid unnecessary macros like:

1 macro_rules! PI {
2 () => {
3 3.141593
4 };
5 }
6 macro_rules! BEGIN {
7 () => {
8 {
9 };
10 }
11 macro_rules! END {
12 () => {
13 }
14 };
15 }

And beware of dangerous macros:

1 macro_rules! SQUARE {
2 ($a:expr) => {
3 $a * $a
4 };
5 }
6 macro_rules! INCR {
7 ($xx:expr) => {
8 $xx += 1
9 };
10 }

Expanding SQUARE!(x + 2) results in (x + 2) * (x + 2) , leading to incorrect calculations.


Instead, always use parentheses:

1 macro_rules! MIN {
2 ($a:expr, $b:expr) => {
3 if $a < $b {
4 $a
5 } else {
6 $b
7 }
8 };
9 }

Even with parentheses, macros can cause side effects:

1 let mut x = 1;
2 let mut y = 10;
3 let z = MIN!(x += 1, y += 1); // x becomes 3; y becomes 11

When defining macros, it is often necessary to create new names. A string can be created by
concatenating two strings using the concat_idents! macro:

1 macro_rules! NAME2 {
2 ($a:ident, $b:ident) => {
3 concat_idents!($a, $b)
4 };
5 }

To convert a parameter to a string, use the stringify! macro:

1 macro_rules! printx {
2 ($x:ident) => {
3 println!("{} = {}", stringify!($x), $x);
4 };
5 }
6 let a = 7;
7 let str = "asdf";
8 fn f() {
9 printx!(a); // prints "a = 7"
10 printx!(str); // prints "str = asdf"
11 }

Use #[macro_export] to ensure no macro called X is defined, protecting against unintended


effects:

1 #[macro_export]
2 macro_rules! EMPTY {
3 () => {
4 println!("empty");
5 };
6 }
7 EMPTY!(); // prints "empty"
8 EMPTY; // error: macro replacement list missing

An empty macro argument list is often error-prone or malicious. Macros can even be variadic:

1 macro_rules! err_print {
2 ($($arg:tt)*) => {
3 eprintln!("error: {}", format_args!($($arg)*));
4 };
5 }
6 err_print!("The answer is {}", 42); // prints "error: The answer is 42"

In summary, while macros can be powerful, their use in Rust should be minimal and well-considered,
favoring more robust and clear alternatives whenever possible.

14.26. Conditional Compilation


One essential use of macros is for conditional compilation. The #ifdef IDENTIFIER directive
includes code only if IDENTIFIER is defined. If not, it causes the preprocessor to ignore subsequent
input until an #endif directive is encountered. For example:

1 fn f(a: i32
2 #ifdef ARG_TWO
3 , b: i32
4 #endif
5 ) -> i32 {
6 // function body
7 }

If a macro named ARG_TWO is not defined, this results in:


1 fn f(a: i32) -> i32 {
2 // function body
3 }

This approach can confuse tools that rely on consistent programming practices.
While most uses of #ifdef are more straightforward, they must be used judiciously. The #ifdef and
its complement #ifndef can be harmless if applied with restraint. Macros for conditional compilation
should be carefully named to avoid conflicts with regular identifiers. For instance:

1 struct CallInfo {
2 arg_one: Node,
3 arg_two: Node,
4 // ...
5 }

This simple source code could cause issues if someone writes:

#define ARG_TWO x

Unfortunately, many essential headers include numerous risky and unnecessary macros.
A more robust approach to conditional compilation involves using attributes like #[cfg] and #
[cfg_attr] , which integrate into the language more seamlessly and avoid the pitfalls of macros. This
method ensures cleaner and safer code management.

14.27. Predefined Macros


Predefined macros in Rust provide similar functionality to assist with debugging and conditional
compilation:
file!() : Expands to the current source file's name.

line!() : Expands to the current line number within the source file.

column!() : Expands to the current column number within the source file.

module_pah!() : Expands to a string representing the current module path.

cfg!() : Evaluates to true or false based on the compilation configuration options.

These macros are useful for providing contextual information for debugging and logging. For instance,
the following line of code prints the current line number and file name:

println!("This is line {} in file {}", line!(), file!());

Conditional compilation is handled using the #[cfg] attribute, enabling or disabling code based on
specific configuration options:

1 #[cfg(debug_assertions)]
2 fn main() {
3 println!("Debug mode is enabled");
4 }
5
6 #[cfg(not(debug_assertions))]
7 fn main() {
8 println!("Release mode is enabled");
9 }

Additionally, custom configuration options can be defined using the --cfg flag in rustc or within
Cargo.toml . By using the cfg! macro, you can conditionally execute code blocks based on the
target operating system or other compile-time conditions:

1 if cfg!(target_os = "windows") {
2 println!("Running on Windows");
3 } else {
4 println!("Running on a non-Windows OS");
5 }

These predefined macros and conditional compilation features enhance flexibility and robustness,
making it easier to manage code based on the compilation environment.

14.28. Pragmas
Platform-specific and non-standard features can be managed using attributes, which are similar to
pragmas in other languages. Attributes provide a standardized way to apply configuration options,
hints, or compiler directives to the code. For instance:

#![feature(custom_attribute)]

Attributes can be applied at various levels, such as modules, functions, and items. While it is generally
best to avoid using non-standard features if possible, attributes provide a powerful tool for enabling
specific functionality. Here are a few examples of attributes:
#[allow(dead_code)] : Suppresses warnings for unused code.

#[inline(always)] : Suggests that the compiler should always inline a function.

#[deprecated] : Marks a function or module as deprecated.

14.29. Advices
In Rust, organizing your code into well-defined, clearly named functions is crucial for enhancing
readability and maintainability. Functions serve as a means to break down complex tasks into smaller,
more manageable pieces, each focusing on a single, coherent task. This modular approach not only
makes the code easier to understand but also simplifies its maintenance.
When declaring functions, you specify the function’s name, parameters, and return type. This
declaration acts as a blueprint, providing an overview of what the function does and what it requires. In
Rust, functions should be designed to handle specific tasks and remain succinct. Keeping functions
short helps maintain clarity, making them easier to understand and debug.
Function definitions in Rust provide the actual implementation of the function. It is essential that these
definitions are straightforward and efficient, aligning with the function's declared purpose. Avoid
returning pointers or references to local variables. Instead, Rust’s ownership and borrowing rules
ensure that returned values are either owned or have appropriately scoped references, preventing
issues like dangling references.
For functions that require compile-time evaluation, you should use const fn . This feature allows
functions to be evaluated during compilation, which can enhance performance by reducing runtime
computations. If a function is designed to never return normally, such as one that loops indefinitely or
exits the process, you should use Rust’s ! type to denote that the function does not return.
When passing arguments to functions, it is advisable to pass small objects by value for efficiency,
while larger objects should be passed by reference. Rust’s ownership system ensures that references
are used safely, adhering to borrowing rules to avoid mutable aliasing and data races. For complex
types, using &T or &mut T allows you to manage immutability and mutation effectively.
Design functions to return results directly rather than modifying objects through parameters. This
approach aligns with Rust’s ownership and borrowing features, allowing for safe and clear
management of data through move semantics and references. Avoid using raw pointers where feasible
and rely on Rust’s type system to ensure safe and efficient data handling.
In Rust, macros can be powerful but should be used sparingly. Functions, traits, and closures are
generally preferred for most tasks due to their clarity and maintainability. When macros are necessary,
use unique and descriptive names to reduce potential confusion and maintain transparency in your
code.
For functions involving complex types or multiple arguments, use slices or vectors rather than variadic
arguments. This approach improves type safety and clarity. Rust does not support traditional function
overloading, so to handle similar functionalities with varying inputs, consider using descriptive function
names or enums. Similarly, document preconditions and postconditions clearly to enhance the
correctness and readability of your functions.
By adhering to these practices, Rust programmers can create well-structured and efficient code.
Leveraging Rust’s features for ownership, borrowing, and compile-time evaluation ensures that code
remains safe, maintainable, and performant.

14.30. Further Learning with GenAI


Assign yourself the following tasks: Input these prompts to ChatGPT and Gemini, and glean insights
from their responses to enhance your understanding.
1 Describe why functions are crucial in Rust programming. Discuss how they help in breaking
down complex tasks, improving code readability, and enhancing maintainability. Provide
examples of how well-defined functions contribute to clear and structured code.
2 Provide an overview of how to declare functions in Rust. Discuss the components of a
function declaration, including the function name, parameters, and return type. Offer
examples to illustrate how function declarations set up the blueprint for function
implementations.
3 Break down the elements of a function declaration in Rust. Explain the significance of each
part, such as the function’s name, parameters, and return type. Discuss how these
components contribute to the function’s role and behavior within a program.
4 Explore how to define functions in Rust. Explain the syntax and structure of function
definitions, including how to provide the function body and implement its logic. Use examples
to demonstrate the process of turning function declarations into executable code.
5 Discuss the concept of returning values from functions in Rust. Explain how functions can
return different types of values and how to handle these return types. Provide examples of
functions that return values and those that do not return any value.
6 Examine the use of inline functions in Rust and their impact on performance. Explain the #
[inline] attribute and how it suggests to the compiler that a function might benefit from
inlining. Provide examples of scenarios where inlining can enhance performance.
7 Describe the use of const fn in Rust for compile-time evaluation of functions. Discuss how
const fn functions differ from regular functions and provide examples of how they can be
used to perform computations during compilation.
8 Explore how conditional evaluation is managed within Rust functions. Discuss how to use
conditional statements and cfg attributes to include or exclude code based on specific
conditions. Provide examples to illustrate conditional logic in function definitions.
9 Explain how to use the ! type for functions that are intended to never return. Discuss
scenarios where such functions are useful, such as in infinite loops or functions that terminate
the program. Provide examples to demonstrate the use of ! as a return type.
10 Discuss different methods for passing arguments to functions in Rust. Explain the use of value
passing, reference passing, and how to handle arrays and lists. Provide examples that show
how to define, initialize, and manipulate function arguments effectively.
Diving into the world of Rust functions is like embarking on a thrilling exploration of a new landscape.
Each function-related prompt you tackle—whether it’s understanding function declarations, optimizing
performance with inline functions, or mastering argument passing—is a key part of your journey
toward programming mastery. Approach these challenges with an eagerness to learn and a readiness
to discover new techniques, just as you would navigate through uncharted terrain. Every obstacle you
encounter is an opportunity to deepen your understanding and sharpen your skills. By engaging with
these prompts, you’ll build a solid foundation in Rust’s function mechanics, gaining both insight and
proficiency with each new solution you develop. Embrace the learning journey, stay curious, and
celebrate your milestones along the way. Your adventure in mastering Rust functions promises to be
both enlightening and rewarding. Adapt these prompts to fit your learning style and pace, and enjoy
the process of uncovering Rust’s powerful capabilities. Good luck, and make the most of your
exploration!

navigate_beforeSelect
Chapter 13
Operations
Chapter 15
Exception Handling
navigate_next

You might also like