2.19. The struct Value Type

The struct mechanism allows us to introduce new value types into our application. The declaration looks exactly the same as for a class, except that we use the struct keyword—for example,

public struct matrix
{
    private double[,] m_mat;
    private int       m_row;
    private int       m_col;

    // ...
}

A value type stores its data directly within the object. A matrix object, for example, directly holds the m_row and m_col integer values, as well as the handle to the two-dimensional reference type array, m_mat.

When we initialize or assign one struct object with another, a deep copy is carried out; the two objects hold the same values but remain independent, unlike the shallow-copy semantics of a reference type. This both simplifies and speeds up our addition operation—for example, since we just bitblast the one matrix object into a second:

public static matrix operator+( matrix m1, matrix m2 )
{
    check_both_rows_cols( m1, m2 );
    matrix mat = m1;

    for ( int ix = 0 ; ix < m1.rows; ix++ )
          for ( int ij = 0; ij < m1.cols; ij++ )
                mat[ ix, ij ] += m2[ ix, ij ];

    return mat;
}

A struct object is not allocated on the managed heap and is therefore not subject to garbage collection. When we create a new struct object, such as

public void func()
{
    matrix mat = new matrix();
    // ...
}

the new operator is not actually invoked. The matrix object is allocated directly within the function. It comes into existence when the function begins execution and ceases to exist when the function terminates.

Every value type is automatically provided with a default constructor—that is, a constructor that takes no arguments. The default constructor zeros out the data members stored within the object of the value type. It is an error to provide an explicit default constructor within a struct definition.

Invoking the new operator on a value type simply results in the execution of an associated constructor. If no arguments are passed to the invocation, the default constructor is invoked. If we provide arguments, the compiler searches for a constructor matching those arguments. For example, the statement

matrix mat = new mat( 4, 4 );

requires that we provide a matrix constructor that takes two arguments:

public struct matrix
{
    public matrix( int row, int col )
    {
        m_row = ( row <= 0 ) ? 1 : row;
        m_col = ( col <= 0 ) ? 1 : col;

        m_mat = new double[ m_row, m_col ];
    }
}

In addition to the restriction preventing a struct from introducing a default constructor, there are two other constraints: (1) The declaration of data members cannot include an initializer, and (2) a struct cannot provide a destructor.

This turns out to be problematic under some circumstances. For example, in computer graphics we generally define a specialized 4x4 matrix to perform geometric transformations such as rotation and scaling. Because we cannot define either a default constructor or an explicit member initializer, there is no straightforward way to define a 4x4 matrix as a struct—for example,

public struct Matrix44
{
   private double[,] mat; // the problem ...

The problem is that the array representing the matrix is a reference type. Its declaration does not represent the size of its two dimensions. Within a class definition, we would add the dimension, as in the following example:

public class Matrix44
{
   private double[,] mat = new double[ 4, 4 ];

but we can't do that inside a struct. Similarly, in the class definition we could have initialized mat within the default constructor. But we can't provide a default constructor within a struct.

There are tricks around these constraints, but having to resort to tricks is somewhat disappointing. In the sample programs for the book, I've provided a simple example of how one might program around these constraints. It is called structFixMatrix.

Why would we define a struct rather than a class? For improved runtime performance, primarily. For example, if SmallInt is a struct, the declaration

SmallInt [] vertices = new SmallInt[ 1000 ];

creates an array of a thousand SmallInt objects. If SmallInt is a class, the declaration results in an array of a thousand null handles. Each of the thousand objects still needs to be allocated on the managed heap through an additional thousand calls to operator new.

Remember that reference types declared as struct members still result in a shallow copy. That is, if our Matrix44 struct contains an array reference type member, copying one Matrix44 to another by default results in both array members referring to the same heap array object.

When do we choose to make an abstraction a struct? The abstraction should be small; otherwise the deep-copy semantics can boomerang, particularly when we pass objects into functions by value. In addition, the abstraction should be heavily used; otherwise the performance benefit will probably be negligible. The predefined arithmetic types—int, double, and so on—are defined as value types under the .NET framework.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.59.107.152