Strict mode for C++
Early draft proposal

John Nagle
Animats

Code snippets of "strict mode" C++

 

Introduction

To illustrate the use of the proposed "strict mode" for C++, some code snippets are provided. This set of snippets will be added to over time.

Pointer semantics in strict mode is described here.

Strostrup's first questions

Bjarne Strostrup sent us the following message. Code snippets addressing these issues appear below.

Please give us an idea of how you would like to handle the most obvious
problems. For example, array range checking, zero-pointers, dangling
pointers and unions:

        union U {
                int i;
                int* p;
        };

        void f(int* p1, int* p2, int* p3, U u)
        {
                *p1 = 1;
                *p2 = 2;
                p3[7] = 3;
                u.p = 4;
        }

        void g()
        {
                int* p = new int;
                delete p;

                int* a = new int[4];

                U u;
                u.i = 5;

                f(0,p,a,u);
        }       


        - Bjarne
Bjarne Stroustrup - http://www.research.att.com/~bs

Unions

Standard
union U {
    int i;
    int* p;
};

Strict

(minimal
changes)

union U {
    int i; 
    int* p; // ERROR - not permitted
};

Strict

(better
style)

class U {
public:
    virtual void seti(int iin) { throw("Wrong subclass"); }
    virtual void setp(int* pin) { throw("Wrong subclass"); }
    virtual int geti() const { throw("Wrong subclass"); }
    virtual int* getp() const { throw("Wrong subclass"); }
};
class Ui: public U {
    int i;
public:
    void seti(int iin) { i = iin; }
    int geti() const { return(i); }
};
class Up: public U {
    int* p;
public:
    void setp(int* pin) { p = pin; }
    int* getp() const { return(p); }
};

It's not clear from the source code what the purpose of such a union would be. There are two basic reasons for writing such a construct: to create a variant record for storing different data types, or to forcibly override the type system. In strict mode, these uses must be distingushed.

The variant record case can, of course, be written with classes and subclasses. Admittedly this is verbose. But it is safe, and accepted C++.

If it is absolutely necessary to override the type system reinterpret_cast provides means for doing so. This are easy to find during program inspection, and verbose enough to discourage its unnecessary use.

Unions that don't imply pointer conversions are still allowed. Such unions are memory safe, so there is no need to disallow them in strict mode. This allows unions in their most used situation, where data of unknown format is being obtained from an external source.

Pointers

Standard
void f(int* p1, int* p2, int* p3, U u)
{
    *p1 = 1;
    *p2 = 2;
    p3[7] = 3;
    u.p = 4;
}
Strict
(minimal changes)
void f(auto int* p1, auto int* p2, auto valarray<int>& p3, U u)
{
    *p1 = 1;
    *p2 = 2;
    p3[7] = 3;
    u.p = reinterpret_cast<int*>(4); // do you really want to do this?
}
Strict
(better style)
void f(auto int& p1, auto int& p2, auto valarray<int>& p3, U u)
{
    p1 = 1;
    p2 = 2;
    p3[7] = 3;     // using a collection class
    u.seti(4);
}

In strict mode, pointers are by default "strong pointers", providing safety at a run-time cost. Pointers are pointers to single objects; STL collections must be used in place of C arrays.

The use of auto in the "better style" example makes the references temporary, which eliminates reference counting overhead within f. The restriction imposed by auto is that the reference can't be assigned to something that might outlive it.

If it's absolutely necessary to force an integer into a machine pointer, the syntax reinterpret_cast<int*>() will do it. This is hard to write and easy to find in source code.

New and delete

Standard
void g()
{
    int* p = new int;
    delete p;
    int* a = new int[4];
    U u;
    u.i = 5;
    f(0,p,a,u);
}       
Strict
(minimal changes)
void g()
{
strict_ptr<int> p = strict_new<int>;
//delete p; // explicit delete not supported
strong_ptr<valarray<int> > a = strong_new<valarray<int>>(4);
U u;
u.p = reinterpret_cast<int*>(5); // Do you really want to do this? f(0,p,a,u);
}
Strict
(better style)
void g()
{
auto int& p = new int;
strong_ptr<valarray<int> > a = strong_new<valarray<int>>(4);
U u;
u.seti(5);
f(0,p,a,u); // ERROR - can't pass 0 to a ref.
}

Note that we passed a strong pointer to an auto scope pointer. This is an allowed automatic conversion. Any function argument which can be a simple auto scope pointer should be, because reference counting overhead in the function is then eliminated.

In the "better style" version, which uses references as inputs to a function which clearly expects non-null input, the passing of 0 as a reference is caught at compile time. This is standard C++. In the "minimal changes" version, we pass 0 as a pointer value, which is legitimate, and then catch the resulting dereference of 0 inside f at run time. References thus tend to have better performance than pointers.

 

July 8, 2001