ITworld.com
  Search  
ITworld Home Page ITworld Webcasts ITworld White Papers ITworld Newsletters ITworld News ITworld Topics Careers ITworld Voices ITwhirled Changing the way you view IT
 

Crossing the language barrier

ITworld.com 8/7/00

Type systems vary greatly from language to language. Which of these four software-modeling rules can help us grasp the differences?

Michael L. Perry, ITworld.com

Developers tend to divide along language boundaries. Once we know a programming language, we identify ourselves by it -- we're "a C++ programmer," "a Delphi developer," etc. One explanation for this tendency to affiliate is that old barrier to change that naturally exists between programming languages: familiarity. People who have spent time learning one set of rules are naturally reluctant to put them aside in favor of another set. As a wise, green little man once said, "You must unlearn what you have learned."

The key is applicability. I prefer to think of each programming language as a specialized tool -- and I recognize that a hammer specialist does not make a good carpenter. I find C++, Delphi, and Java to all be useful languages, and I even apply a little VB when appropriate. The four rules of software modeling -- identity, interface, ownership, and dependency -- have helped me to understand and learn each new language quickly.

The rule of identity, for instance, helps me understand the differences between the type systems of C++ and Delphi.

C++ pointers and references
C++ extends the type system of its ancestor language. C offers a set of built-in types, including integers, characters, and floating-point numbers. It also provides constructs, such as structures and unions, with which a developer can compose application-specific types.

Declarations
A variable declared as any built-in or composite type represents a unique identity; during compilation, the compiler automatically allocates space in the variable's name. During its life span, the variable represents only one object, the owner of which is defined by the variable's scope.

For example:


  struct
  {
    int n;
  } s;
  /* The structure identified by s
     owns the integer identified by n.
     The integer exists only within
     one structure, and only so long
     as the structure exists. */

Pointers
A pointer in C (and, by extension, C++) does not represent a unique identity. It instead refers to the identity of an object allocated independent of the pointer itself.

Such an object could be allocated automatically, as in this example:


  int n;
  int *p = &n;

The object could also be allocated manually:


  int *p = (int *)malloc( sizeof(int) );
  free( p );

A pointer's scope, unlike a value's, does not confer ownership.


  int n;
  struct
  {
    int *p;
  } s1, s2;
  s1.p = &n;
  s2.p = &n;
  /* Neither s1 nor s2 owns n. */

The behavior of C's (and C++'s ) assignment and comparison operators depends on whether they're applied to values or pointers. The assignment operator (=) copies state when applied to values and identity when applied to pointers. Similarly, the comparison operators (== and !=) compare state when applied to values and identity when applied to pointers:

 
  int n1, n2;
  int *p1, *p2;
  n1 = 5;   /* Copy the value 5 into */
  n2 = 5;   /* two unique integers. */
  p1 = &n1; /* Copy the identities of */
  p2 = &n2; /* n1 and n2 into p1 and p2. */
  _ASSERT( n1 == n2 ); /* Compare values. */
  _ASSERT( p1 != p2 ); /* Compare identities. */

Furthermore, a pointer can refer to different objects during its life span:

 
  int n1, n2;
  int *p;
  p = &n1; /* p refers to n1. */
  p = &n2; /* Now, p refers to n2. */

C uses a special syntax to obtain the value of a pointer. In this syntax, the assignment and comparison operators revert to their value semantics:


  int n1, n2;
  int *p1, *p2;
  p1 = &n1; /* Assign identities. */
  p2 = &n2;
  *p1 = 5; /* Assign values. */
  *p2 = 5;
  _ASSERT( p1 != p2 ); /* Compare identities. */
  _ASSERT( *p1 == *p2 ); /* Compare values. */

References
C++ introduced references to provide for the representation of identity without the need for a special syntax. A reference is similar to a pointer, insofar as it holds the identity of an object separate from itself and its scope does not define ownership:

 
  int n;
  struct s
  {
    int &m_r;
    s( int &r ): m_r( r ) {}
  } s1(n), s2(n);
  // Both s1 and s2 refer to n, yet
  // neither owns it.

A reference differs from a pointer in that it cannot change identity during its life span and all operators use value semantics:

 
  int n1 = 5;
  int n2 = 5;
  int &r1 = n1; // Initialization is the only time
  int &r2 = n2; // that identity can be assigned.
  _ASSERT( r1 == r2 ); // Compare values.

Delphi 'references'
Let's compare C++'s type system with Delphi's. In Delphi, as in C++, variables of built-in and record types define values. Unlike C++, however, Delphi treats all class and interface variables as references. Don't let the name confuse you; a Delphi reference has more in common with a C++ pointer than with a C++ reference.

Like a C++ pointer, a Delphi reference refers to the identity of an object separate from itself. Delphi assignment and comparison operators copy and compare identity, not value. Furthermore, a reference variable can refer to different objects during its life span:


type
  TMyClass = class(TObject)
  public
    n: integer;
  end;

procedure Test;
var
c1, c2: TMyClass;
begin
{ c1 and c2 are initially nil. }
c1 := TMyClass.Create; { Assign identity. }
c2 := TMyClass.Create;
c1.n := 5; { Assign values. }
c2.n := 5;
Assert( c1 <> c2 ); { Compare identities. }
Assert( c1.n = c2.n ); { Compare values. }
FreeAndNil( c1 );
FreeAndNil( c2 );
end;

Stumbling blocks
The differences between the type systems of C++ and Delphi are potential stumbling blocks. Because the languages implement identity in different ways, developers must be careful when commuting from one to the other.

The following C++ code, for example, was ported from a Delphi program. The Delphi code worked just fine, but the C++ code does not. Can you spot the bug?


// Base class of a real-valued function.
class CRealFunction
{
public:
  virtual double GetValue( double x )
    { return 0.0; }
};

// A specific real-valued function that solves for
// the exponent of a decline curve given two
// points and the initial rate of decline.
class CRealFunctionDeclineByN: public CRealFunction
{
public:

  CRealFunctionDeclineByN(
    double x0,
    double y0,
    double x1,
    double y1,
    double d0) :
      m_x0(x0),
      m_y0(y0),
      m_x1(x1),
      m_y1(y1),
      m_d0(d0) {}

  double GetValue( double x )
    {
      double n = x;
      return m_y1 - m_y0/pow(
        1 + m_d0*n*(m_x1-m_x0), 1.0/n );
    }

private:
  double m_x0;
  double m_y0;
  double m_x1;
  double m_y1;
  double m_d0;
};

inline bool Opposite( double d1, double d2 )
{
  return (d1 < 0.0) && (d2 > 0.0) ||
    (d1 > 0.0) && (d2 < 0.0);
}

inline bool IsZero( double d )
{
  const double eps = 1e-14;
  return (d > -eps) && (d < eps);
}

// Find a zero using the bisection method.
double FindZero_Bisection(
  CRealFunction f,
  double xlow,
  double xhigh )
{
  double ylow;
  double yhigh;
  double xmid;
  double ymid;

  // Verify that a zero is bracketed.
  ylow = f.GetValue( xlow );
  yhigh = f.GetValue( xhigh );
  if (!Opposite( ylow, yhigh ))
    AfxThrowUserException();

  do
  {
    // Bisect the brackets.
    xmid = ( xlow + xhigh )*0.5;
    ymid = f.GetValue( xmid );
    // Keep the half that contains the zero.
    if (Opposite( ymid, ylow ))
    {
      yhigh = ymid;
      xhigh = xmid;
    }
    else
    {
      ylow = ymid;
      xlow = xmid;
    }
  }
  while ( !IsZero(ymid) && !IsZero(xlow-xhigh) );

  return xmid;
}

// A simple unit test.
void Test()
{
  double n;
  CRealFunctionDeclineByN f(
    0.0, 10.0, 1.0, 5.8, 1.0 );
  char strMessage[512];

  n = FindZero_Bisection( f, 1e-3, 10.0 );
  sprintf( strMessage, "n = %g, y = %g",
    n, 10.0/pow( 1.0 + n, 1.0/n ) );
  AfxMessageBox( strMessage, MB_OK, -1 );
}

Next week, I'll reveal the bug and talk about what identity is good for in the first place.

Michael L. Perry has been a professional Windows developer for over six years and maintains expertise in COM+, Java, XML, and other technologies currently shaping the programming landscape. He formed Mallard Software Designs in 1998, where he applies the mathematical rigor of proof -- establishing the correctness of a solution before implementing it -- to software design. He is the moderator of ITworld.com's Windows Application Development discussion.





 
www.itworld.com    open.itworld.com     security.itworld.com     smallbusiness.itworld.com
storage.itworld.com     utilitycomputing.itworld.com     wireless.itworld.com

 
Contact Us   About Us   Privacy Policy    Terms of Service   Reprints  

CIO   Computerworld   CSO   GamePro   Games.net   Industry Standard   Infoworld   ITworld  
JavaWorld   LinuxWorld  MacUser   Macworld   Network World   PC World   Playlist  

DEMO   IDG Connect   IDG Knowledge Hub   IDG TechNetwork   IDG World Expo  

Copyright © Computerworld, Inc. All rights reserved

Reproduction in whole or in part in any form or medium without express written permission of Computerworld Inc. is prohibited. Computerworld and Computerworld.com and the respective logos are trademarks of International Data Group Inc.