Skip to content

Clay for C++ programmers

jckarter edited this page Nov 25, 2010 · 33 revisions

Clay vs C++: Executive summary

Clay's primary goal is to be a systems programming language, with a focus on performance and generic programming. Clay thus shares many features with common C++ implementations:

  • Value semantics, and Resource-Acquisition-is-Initialization (RAII)
  • Raw memory access, including pointers and pointer arithmetic
  • Minimal runtime dependencies—no required VM or garbage collector
  • ABI compatibility with C
  • Templates (called generics in Clay)
  • Raw machine access through inline LLVM and inline machine assembly language
  • ...

However, Clay also discards many C++ features:

  • Source compatibility with C
  • Language-level support for object-oriented programming (although Clay is flexible enough in which to implement a custom object system and to provide bindings to foreign object systems such as Cocoa, glib, or COM)
  • ...

On the other hand, it adds many new features over C++98:

  • Pervasive type propagation—nearly all types can be inferred
  • Generic specialization based on type predicates
  • Multiple dispatch through variant types
  • ...

and has many features also added by C++0x:

  • Variadic generics and types
  • Lambdas
  • Perfect forwarding
  • ...

Comparing "Hello World"

In C++:

  #include <iostream>
  int main() { std::cout << "Hello world!\n"; }

In Clay:

  main() { println("Hello world!"); }

This simple example introduces some basic differences between C++ and Clay:

  • Clay's io library is part of its prelude, a module implicitly available to every Clay module, so there is no need to include a header file to access the println function.
  • main does not need to declare its return type, because Clay will infer the type from the body of the function. Since there is no return statement, Clay infers that it returns no values. Clay also allows main to return an integer value back to the OS, as in C++.
  • Clay's println is a variadic function that prints zero or arguments to stdout, followed by a newline.

Comparing type systems

Clay's set of fundamental types is similar to C and C++'s:

  • Sized integers: Int8, Int16, Int32, Int64 (aliased as Byte, Short, Int, and Long, respectively)
  • The corresponding unsigned integers: UInt8 etc. (with corresponding UByte etc. aliases)
  • Floating-point numbers: Float32, Float64 (aliased as Float and Double)
  • Pointers: Pointer[T] for any type T
  • Arrays: Array[T,n] for any type T and integer size n

To ease communication with C libraries, Clay's standard library defines aliases CChar, CShort, CInt, CLong, CLongLong, CUChar, etc. that match the standard sizes for the corresponding C types.

These types behave mostly similarly to their C++ counterparts; however, there are some minor differences:

  • Clay never implicitly converts between types, unlike C++. When calling functions, the parameter types must match exactly, even among integer or float types:
  foo(x: Int, y: Int) = x + y; // Function takes two Ints, returns an Int
  main() { println(foo(2u, 3u)); } // ERROR: 2u and 3u are UInt but foo expects Ints

However, functions can be made generic on their inputs and perform conversions on behalf of their callers. See the Generics section for details.

  • Clay's Arrays have value semantics and do not degenerate into pointers when used in expressions or passed to functions, so this is valid and will copy x into y:
  var x = [1, 2, 3];
  var y = [4, 5, 6];
  y = x;

Clay also provides some fundamental data structure types:

  • Tuples: Tuple[...T] for any set of types ...T. These provide anonymous structure types, similar to boost::tuple or std::tuple introduced in C++0x.
  • Unions: Union[...T] for any set of types ...T. Unlike C or C++'s unions, Clay Unions are anonymous like Tuples.

Clay has a special generic type Static[x] without a clear analog in C++. When used as a runtime value, a Clay function or type name x manifests itself as a value of the type Static[x]; for example, Int is of type Static[Int] and main is of type Static[main]. Integers and floats can also be introduced as static values using the static keyword: static 0 gives a value of the type Static[0]. A value of any of these types is empty and indistinguishable from any other value of the same type; in other words, every type Static[x] is a singleton type. ...

Comparing strings

Comparing container libraries

Comparing object lifecycle

Clay and C++ both allow custom types to implement custom value semantics and to control object lifecycle by overloading copy construction, destruction, and assignment. A classic example is a string type that allocates a dynamic buffer in which to store the string contents. To safely manage its resources, the string type must allocate a new buffer when it is copied or assigned, and free the buffer when it is in destroyed. In C++, this is implemented using constructors, destructors, and operator= within a class:

  #include <cstring>
  using std::strdup;
  using std::free;

  class silly_string {
  private:
      char *str;
  public:
      silly_string(char const *s) : str(strdup(s)) {}

      silly_string(silly_string const &s)
          : str(strdup(s.str)) {}

      ~silly_string() { free(str); }
      
      silly_string &operator=(silly_string const &s) {
          free(str);
          str = strdup(s.str);
      }
  };

In Clay, constructors, destructors, and assignment are free functions. A type name also acts as a constructor function for that type. Destruction is performed by the destroy function, and assignment by the assign function:

  import libc.(strdup, free);
  
  record SillyString (str: Pointer[CChar]);
  
  overload SillyString(s: Pointer[CChar]) returned: SillyString {
      returned.str <-- strdup(s);
  }
  overload SillyString(s: SillyString) = SillyString(strdup(s.str));
  
  overload destroy(s: SillyString) { free(s.str); }
  
  overload assign(to: SillyString, from: SillyString) {
      free(to.str);
      to.str = strdup(from.str);
  }

(more commentary)

Clone this wiki locally