[Phy 405/905 Home Page] [ Lecturer ]

1. (part 2 of) Types

In a continuation of our discussion of types, we'll see how functions are declared and defined, and then take up the question of type conversion - automatic and explicit.


Declaring and defining functions

Declaring functions

Functions must be declared before being used, which means that the argument(s) and return value types are specified:

int main()
{
// some code ...

// declaration of my_sqrt(), which takes a double, returns a double
extern double my_sqrt(double x);

double result = my_sqrt(4.0); // my_sqrt() has been declared,
//can use it now
Some points:

Header files

To avoid error, it's usually best to put all the function declarations (and some other things as well! we'll get to that later) in one or more header files. If main() is contained in prog.cc, my_sqrt() in subs.cc, we might create a file called "subs.hh" which contains the declarations for all the functions in subs.cc:
// contents of subs.hh
#ifndef SUBS_HH_INCLUDED // ensure this file only gets included ONCE
#define SUBS_HH_INCLUDED

// declarations of functions
extern double my_sqrt(double x);

#endif

Then we would insert an extra line in prog.cc near the top of the file, right around the #include for <iostream.h> that appears in all these examples

// top of prog.cc
#include <iostream.h> // make i/o functions accessible
#include "subs.hh" // start file scope of my_sqrt() and other functions
Basically what happens is the preprocessor, (which is invoked automatically before each compilation of a file) goes through and inserts the contents of "subs.hh" into prog.cc, which thus declares my_sqrt() and introduces into the file scope of prog.cc. If you wanted, you could insert another declaration of my_sqrt() into the code (declarations can be repeated as desired, but not definitions), but its not necessary nor what I usually do.

The lines in "subs.hh" beginning with "#" are preprocessor directives, in this case put in to ensure that subs.hh never gets included more than once in a file (to avoid circular inclusion). It's a good practice to include those three lines in all your header files, changing the "SUBS_HH" in "SUBS_HH_INCLUDED" to the name of the header file.

Header files (e.g., stdlib.h, subs.hh, math.h, ...) are convenient places to look when you don't remember how a particular function is called.

Defining functions

Sooner or later we have to get around to defining the function my_sqrt() (providing its source code), which we choose to do in subs.cc:
// subs.cc
#include "subs.hh" // get consistent declaration of my_sqrt()
#include <math.h> // get declaration of standard math functions

double my_sqrt(double x)
{
return sqrt(x);
}
Pretty simple function, but it illustrates the essential points:

What good are declarations?

// somewhere in your program
#include "subs.hh"
int main()
{
// (1) okay
double y_double = 57.3e0;
double x_result = my_sqrt(y_double);

// (2) not good? Very undefined in Fortran!!
// Much gnashing of teeth and pulling of hair till following
// bug found!!
float y_single = 57.3f0;
double x_double = my_sqrt(y_single);

// (3) even worse?
int y_int = 57;
double x_int = my_sqrt(y_int);
}

What's wrong with (2)? We're passing a single-precision float where a double's expected, i.e., a piece of data that is half as wide as expected. What's likely to happen in the equivalent case in Fortran is a disaster - the called routine foo() would at the least get a very different argument from the correct one (57.3)! And if it tried to change the argument (we'll need references to do that - more on that later), it'd try stuffing a (say) 8-byte number into a 4-byte space, probably overwriting the stack (give a reminder of what that is) and causing some kind of mysterious program crash. Case (3) seems even worse...

But luckily we're writing in C++ (and not Fortran, or even C, see below), where because of the declaration of my_sqrt() in "mydefs.hh", the compiler realizes that there's an argument type mismatch (float instead of double, or int instead of double) and does something about it (standard conversion, covered in the next section).

Now, what if we forgot to declare my_sqrt() in all its glory (i.e., never specified what it took, and what it returned)? In that case, the C++ compiler will issue a compile-time error (i.e., the file won't compile successfully, a hopefully non-cryptic compiler error message will be issued, and you'll fix things pronto). So here's an example of where the compiler checking all the types for appropriateness (type-safety) comes in handy.

Finally, because we put the function declarations in one file, namely "mydefs.hh", we can ensure consistent function declarations over a very large number of files that (over time) get compiled separately. That much less of a headache at link-time, if we are guaranteed that all the files are consistent in their function calls. If we didn't use the #include mechanism, and instead inserted (differing) declarations of my_sqrt in several files that would be compiled and ultimately linked together, the error would still be caught before run-time (avoiding mysterious crashes), but then we'd have to go back and edit and possibly recompile every single affected file.

By the way, ANSI C is laxer than C++ in type-safety. If a function gets used before declaration of its arguments and return values, the compiler assumes it to return a single int and take a single int as its arguments (ANSI C thinks of everything as an int by default), which could delay the problem at worst to the linking stage.

Standard (automatic) Conversions

It's numerically reasonable that there's a natural "identity mapping" from 2u (unsigned int) to 2 (the int) to 2.f0 (single precision floating pt) to 2.e0 (the double precision) to 2.0L (the long double precision).

One ought to be able to do this:

unsigned int i = 2u;
int j = i;
float q = j;
double r = q;
long double s = r;
In fact, C++ does exactly what you expect (and want it to do). This is automatic conversion (in particular, integral promotion). There is bound to be some implementation dependencies here (e.g., converting a large unsigned int to an int).

What about the opposite direction?

long double q = -40.3019231029301L;
double r = q;
int j = r;
unsigned int i = j;
// how about this?
int x = -4;
double z = x + 4.0; // conversion of int to double?

Same thing happens. Basically, the C++ compiler will apply standard conversions in assignments, in arithmetic expressions, and function calls (as well as in explicit conversion or casts, described below) that convert expressions from one type or another. Here's what happens for simple assignments or function calls:

integral promotion
integers shorter than int or unsigned int (char, short int, ...) are automatically converted, if need be, to int (if its wide enough) or unsigned int.
integer demotion
integers can be demoted downward in size (e.g., from long int to short int) via truncation, though the details are implementation dependent - a place to look for rounding effects
float and double conversion
float->double, double->long double conversions clearly can be automatically performed without loss of precision. The opposite paths involve truncation and implementation-dependence
float and integral conversion
integers->floating point type is safe as long as the target type has the necessary precision. Conversion from, e.g., double->int involves truncation of the fractional part (implementation-dependent).
For arithmetic expressions such as + or -, the rules are as follows (where if at any step the condition is true, we stop there)
  1. if either operand (argument of the operator) is long double, the other-> long double.
  2. Repeat this step if necessary for the cases of double, then float
  3. perform integral promotions on any integers
  4. (basically) convert the smaller integer-type operand to the type of the larger integer-type operand

Examples of this:

double x = 4.0L * 1; // integer converted to long-double
char d = 'a' + 4 // char converted to int, then int result->char
int result = my_sqrt(34); // int arg->double, double result->int

Explicit Conversion (casting)

One can also explicitly convert (cast) an objects type, which is useful (and dangerous) when considering pointers. For numeric types, it's used for forcing conversions:
double x = int(my_sqrt(34.0)); // truncate result, convert to double
int int_pi = int( atan(1.0)*4 );
We'll see conversion appear again (importantly) in the context of classes, where conversion can take place via a class constructor.

There's also the cast notation for explicit conversion, which effects the same thing:

double x = (int) 4.23;

Casting has its purposes (see the exercises for an example involving the randum number generator rand()), but usually in the context of low-level machine access or getting access to the "guts" of an object via pointers or references. We'll discuss this again soon, regarding pointer conversion.

[ Phy 405/905 Home Page] [Lecturer ]