about
3/04/2022
Functions

BasicBites - Functions

Free functions, methods, lambdas

Software source code is structured with functions, structs and classes, libraries, executables, and projects. Functions, the lowest of these levels, are named code blocks with input arguments and often a returned value.
f(args) -> returnType { ... }

Function Types

In this section we use the term function for callable code blocks that are not attached to a type, e.g., struct or class. To make that usage clear we may refer to them as unbound or free functions.
  • Free Functions: Free functions are found in native code, e.g., C, C++, and Rust, but not in managed code, e.g., C# and Java. Arguments are passed by value or reference. When passed by value, Copy types are copied onto the function's stack frame while Move types are moved into the stack frame.
    Arguments Passed by Value
    auto f(T t) -> T {...} /* C++ */
    fn f(t:T) -> T {...} /* Rust */
    Pass by value has no side-effects. Should f mutate its copy the caller won't see the change.
    Arguments can also be passed by reference using either language references or pointers.
    Arguments Passed by Reference
    auto f(const T& t) -> T {...} /* pass by const C++ reference */
    auto f(T& t) -> T {...} /* pass by C++ reference */
    fn f(t:&T) -> T {...} /* pass by Rust immutable reference */
    fn f(t:&mut T) -> T {...} /* pass by Rust mutable reference */
    auto f(const T* t) -> T {...} /* pass by C++ pointer to const value */
    auto f(T* t) -> T {...} /* pass by C++ pointer */
    fn f(t:*t) -> T {...} /* pass by unsafe Rust immutable pointer */
    fn f(t:*mut t) -> T {...} /* pass by unsafe Rust mutable pointer */
    Since a reference to the caller's instance is copied to the stackframe, the caller will see a mutation of its instance effected by f.
    If the function is required to change the passed object, not just its value, the only way the caller will see that is if the function passes a reference by reference. for native code that function uses the signature shown below:
    Pass Reference by Reference
    auto f(T** t) -> T {...} /* pass by C++ pointer to pointer */
    fn f(t:T**) -> T {...} /* pass by unsafe Rust pointer to pointer */
    Note that you cannot use C++ or Rust references here because their bindings are fixed once initialized, so the object can't be changed.
  • Methods: Methods are functions that are bound to a type, like struct or class. They are found in both native and managed code, e.g., C++, Rust, and C#. They can use member data of instances of their type, whether the data is declared public or private. Thus all methods share member data of their type. The code, below, illustrates only pass by value, but pass by reference works with essentially the same syntax, except for the parameter types.
    Declaration Implementation Language
    class X {
      auto f(T t);
      ...
    };
      X::f(T t) {...}   /* C++ method */
     
           
    struct X {
      ...
    };
      impl X {
      pub f(t:T) {...}
    }
      /* Rust method */
             
    class X {
      f(T t) {...}
      ...
    };
          /* C# method */
     
         
    Figure 1. Managed Parameter Passing Figure 1. illustrates parameter passing for C#. The ideas are like those discussed at the beginning of Free Functions for C++ and Rust. Here we emphasize that both value types and reference types can be passed by both value and reference. The semantics for those four cases are all different. In Figure 1., v represents an instance of a C# value type, int, double, ... and r represents an instance of a reference type, e.g., user-defined class.
    Argument Passing Syntax for C#
    f(V v) {...} /* pass value type by value */
    f(ref V v) {...} /* pass value type by reference */
    f(R r) {...} /* pass reference type by value */
    f(ref R r) {...} /* pass reference type by reference */
    Pass by value has no side-effects. Should f mutate its copy the caller won't see the change. Pass by non-const reference allows the method to mutate caller's instance through the passed reference.
    C++ and Rust also fit this model. The diagram is the same for those native languages though the bits of code syntax shown on the diagram are different. Also, for C++ and Rust, the objects may reside on the stack of the caller instead of the heap.
  • Lambdas: Lambdas are anonomous code blocks with passed arguments and return values. They are defined locally inside functions, near the site where they are used. In addition they capture any data that is defined in the local block before their definition and used in their bodies. A little detective work shows that a lambda compiles into an anonymous compiler-defined type. The lambda's code block becomes a method of that type and local data become data members of of the new type. That has important implications, e.g., a lambda can be passed into, or returned by, another function. So lambdas are first class objects that can be used like objects defined explicitly by a program. Definitions of lambdas look like this:
    auto lam = [s1](const std::string& s2) {
      std::cout << "\n " << s1 << " of " << s2;
    };
    /* s1 is a local string instance captured by value */
       /* C++ lambda definition */
     
    let mut add = |t: &i32| -> i32 { sum += t; sum };
    /* sum is a local integer that is captured by value */
       /* Rust lambda definition */
     
    Func<int, int, bool> testForEquality = (x, y) => { x == y; };
    /* Func is a standard .net delegate */
       /* C# lambda definition */

Sample Code:

You will find code that illustrates how functions and parameter passing works here:
  Next Prev Pages Sections About Keys