Close
Register
Close Window

Programming Languages

Chapter 6 Variations on Parameter Passing

Show Source |    | About   «  5.2. Tying The Knot   ::   Contents   ::   6.2. Lazy Lists  »

6.1. Parameter-Passing Mechanisms

6.1.1. Call By Value vs. Call By Reference

Author’s Note: All of the visualizations of parameter-passing methods in this were developed by University of Wisconsin Oshkosh CS major Cory Sanin. His work on these has greatly improved the original version of the module.

Parameter-passing techniques may be broken down as follows:

  • Eager evaluation (applicative order) techniques. What these methods have in common is that the arguments passed in for a function’s parameters are evaluated before the function is called.

    • Call-by-value

    • Call-by-reference

    • Call-by-copy-restore (also known as value-result or copy-in-copy-out)

  • Lazy evaluation (normal order) techniques. What these methods have in common is that the evaluation of the arguments passed in for a function’s parameters is delayed until the argument is actually used in the execution of the function.

    • Macro expansion

    • Call-by-name

    • Call-by-need

The difference between call-by-value and call-by-reference is exemplified by the difference between denoted values in our interpreters for SLang 1 and SLang 2. That is, in call-by-value, the argument for a function parameter is a copy of the value of the argument whereas, in call-by-reference, the function is given the address of the argument. Given the address, the function has the capability of modifying the argument.

To see how call-by-value works, step through a few sample programs using the slide show generator below. Once you’re confident that you understand each step, test yourself with the proficiency exercise that follows.

Test yourself on call-by-value by completing the following proficiency exercise.

In comparison to call-by-value, call-by-reference is illustrated by the following slide show generator. Again step through a few of the generated slide shows until you’re ready for the proficiency exercise that follows.

Test yourself on call-by-reference by completing the following proficiency exercise.

Now that you’ve seen the difference between call-by-value and call-by-reference, we will end this section with a problem that will help you review the difference between call by value and call by reference in the language C++, where the presence of an ampersand in front of the parameter’s name is used to indicate call-by-reference semantics. To earn credit for it, you must complete this randomized problem correctly three times in a row.

6.1.2. Copy-Restore

In copy-restore parameter passing, the function is still given the address of the argument, as it was in call-by-reference. However, the protocol for this technique dictates that the function make a copy of the argument before executing the function body. This copy is then worked with in the function body. When the function body has completed, the protocol for copy-restore dictates that the copy of the argument be “restored into” the original argument using the address of the argument, hence potentially modifying that argument. Note that although the original argument is modified, the timing of when the modification occurs is slightly different from what it was under call-by-reference semantics. In the Ada programming language, the programmer could choose to use copy-restore semantics by designating a parameter as an in-out parameter. Although C++ does not offer copy-restore as a parameter-passing technique, we can simulate it in the following C++ code.

#include <iostream>
using namespace std;

void by_value(int a, int b) {
  a = b;
  b = 6;
}
void by_reference(int &a, int &b) {
  a = b;
  b = 6;
}
void by_copy_restore(int &a, int &b) {
  int copya, copyb;
  copya = a;       // copy-in phase
  copyb = b;
  copya = copyb;   // function proper
  copyb = 6;
  a = copya;       // copy-out phase
  b = copyb;
}
int main() {
  int x,y;
  x = 4; y = 5;
  by_value(x, y);
  cout << "Call-by-value semantics: " << x << " " << y << endl;
  x = 4; y = 5;
  by_reference(x, y);
  cout << "Call-by-reference semantics: " << x << " " << y << endl;
  x = 4; y = 5;
  by_copy_restore(x, y);
  cout << "Call-by-copy-restore semantics: " << x << " " << y << endl;
}

As you’ve done with by-value and by-reference, use the following slide show generator to step through a few examples of the copy-restore method and then test yourself by working on the proficiency exercise that follows.

Author’s Note: In the slide show above, the pointers from r and s back to the arguments of the function call exist, and should be shown, as soon as the function is invoked and throughout the execution of the function call.

So, as you can tell from the C++ code above, in call-by-copy-restore, a function parameter corresponds to two values, both a pointer to the corresponding argument and a copy of the value of the argument. First, the copy of the argument’s value is made. Then, the body of the function only uses the copy during its execution. Finally, during the restore phase just before the function returns, the local copy of the argument (i.e., its final value, once the function’s execution has completed) is copied back into the argument.

Note that, when there are more than one parameter, the restore phase takes place for each parameter from left to right in the function’s signature. This order is required by the specification of this parameter-passing mechanism.

Can you think of scenarios in which the left-to-right order of the restore phase matters?

Now, test yourself with a copy-restore proficiency exercise.

We’ve now covered the three parameter-passing methods that use eager evaluation of function arguments.

Before moving on, make sure that you understand why these three methods indeed use eager evaluation.

Now, to compare and contrast these three methods, figure out what the output of the program in the next practice problem would be under call by value, call by reference, and call by copy-restore. Doing this will clarify the subtle differences among these three methods. To earn credit for the following problem, you must complete it correctly for the randomized program it generates three times in a row.

6.1.3. Macro Expansion

Call-by-value, call-by-reference, and call-by-copy-restore all use eager evaluation: The arguments of a function call are evaluated immediately, that is, even before the body of the function is executed.

The remaining three parameter-passing mechanisms use lazy evaluation: The arguments of a function call are passed without being evaluated to the function. Then, during the execution of the function’s body, the parameters are evaluated only when, and as often as, they are needed.

The first lazy-evaluation technique we will discuss is macro-expansion.

Steps involved in macro-expansion are:

  1. No evaluation: The literal text of each argument in the macro call is substituted for the corresponding formal parameter everywhere in the macro’s body.

  2. No evaluation: The body of the macro’s code resulting from Step 1 is textually substituted for the macro call in the caller program.

  3. Evaluation: The body of the macro is executed in the caller’s environment. That is, because of the textual substitution of the macro’s code in the caller program, the scope of the variables involved is determined on the basis of where the macro is called from rather than where the definition of the macro appears in the program. You will see this in the second step of the following slide show, where the code resulting from Step 1 and Step 2 above is presented side-by-side with the original code.

Once you have gone through enough example slide shows to fully understand the details of each step in macro-style parameter passing, test yourself with the following proficiency exercise.

We conclude this section on macro-expansion parameter passing by considering the use of macros in C++, where a parameter like a or b in the example below must be wrapped in parentheses when it is actually used in the body of the macro. Try to determine the output of the main program in each example.

#include <iostream>

using namespace std;

#define by_macro( a, b )  { (a) = (b); (b) = 6; }  // Note parens around use of parameter

int main()
{
  int x,y;

  x = 4; y = 5;
  by_macro(x, y);
  cout << "Call-by-macro semantics: " << x << " " << y << endl;
}
#include <iostream>

using namespace std;

#define by_macro( a, b )  \
         { (a) = (a) + (b); (b) = (a) - (b); (a) = (a) - (b);  }  // Again parens wrap use of param

int main()
{
  int x,y;

  cout << "\nNo aliasing" << endl << endl;
  x = 4;  y = 5;
  by_macro(x, y);
  cout << "Call-by-macro semantics: " << x << " " << y << endl;

  int z;
  cout << endl << endl << "With aliasing" << endl << endl;
  z = 4;
  by_macro(z, z);
  cout << "Call-by-macro semantics: " << z << endl;
}

Implementation of macro-expansion in C++

The implementation of macro-expansion suggested by the 3-step process described previously is to perform a double textual substitution. For example, the C++ pre-processor performs this double substitution, and then the compiler processes the resulting code, never seeing the macro call. Of course, no function call is executed at run-time either.

Because the body of the macro is spliced into the caller’s code after the arguments have been substituted (without being evaluated) for the parameters, the whole body of the macro is executed in the caller’s environment. This allows us to use macro-expansion to simulate dynamic scoping, as illustrated in the following code.

#include <iostream>

using namespace std;

int n = 6;

#define dynamic_scoping  { cout << n << endl; }

void static_scoping()    { cout << n << endl; }

void test_dynamic() {
  int n = 5;
  cout << "Using dynamic scoping --> ";
  dynamic_scoping;
}

void test_static() {
  int n = 5;
  cout << "Using static scoping --> ";
  static_scoping();
}

int main() {
  test_dynamic();
  test_static();
}

The following problem will help you review the differences among call by reference, call by copy-restore, and call by macro. To earn credit, you must complete this randomized problem correctly three times in a row.

6.1.4. Call By Name

In macro expansion, the body of the macro is spliced into the caller’s code after the actual parameters have been substituted (without being evaluated) for the formal parameters. Therefore, the whole body of the macro is executed in the caller’s context (i.e., the caller’s environment).

In call-by-name, no code is spliced into the caller’s code. Instead, the body of the function is executed in its own context, but the actual parameters, which are substituted for the formal parameters, are evaluated in the caller’s context.

Call-by-name differs from macro expansion in that only the parameters are evaluated in the caller’s context, not the whole body of the function. Step through a few slide shows of some call-by-name examples to study the ramifications of this change. When you are confident that you understand the subtleties involved, try the proficiency exercise that follows.

Author’s Note: In the slide show above, the arrows from the parameters to the arguments are NOT actual pointers but rather a way to depict the fact that each parameter has a way (which we’ll describe under the name ‘thunk’ in the next section) to refer back to the arguments in the caller’s environment.

Now it is time for you to do a proficiency exercise to see how well you understand call-by-name. When you do this proficiency exercise, each assignment statement will require two steps. In the first step corresponding to an assignment statement, you will have to compute the value on the right-hand side and then click the location where that value will be stored. In the second step, you will have to click on a potentially new arrow destination resulting from the computation and assignment that comprised your answer for the first step.

The following problem will help you review the differences among call by copy-restore, call by macro, and call-by-name. To earn credit for it, you must complete this randomized problem correctly three times in a row.

6.1.5. Comprehensive Review of the Five Methods Studied So Far

In the next section, we will examine call-by-name versus call-by-need in the context of a specific example known as a lazy list. However, before proceeding, test your comprehensive understanding of all five techniques studied so far: call-by-value, call-by-reference, call-by-copy-restore, call-by-macro, and call-by-name. To earn credit for it, you must complete this randomized problem correctly three times in a row.

   «  5.2. Tying The Knot   ::   Contents   ::   6.2. Lazy Lists  »

nsf
Close Window