MSDN에서 펌
Deep C++  
cl.exe Episode XIII: Attack of the Standards


User-defined Conversions
Conversion from a source type S to a target type T may require a sequence of intermediate implicit conversions along the way. In the simple example

typedef int S[10];
typedef void const *T;

S s;
T t;

t = s; // implicit conversion from S to T

the overall conversion from S to T comprises three implicit conversions in this order:

The array-to-pointer conversion from S to int *, which is really from int [10] to int *.
The pointer conversion from int * to void *.
The qualification conversion from void * to T, which is really from void * to void const *.
You can make any or all of these intermediate steps explicit without changing the meaning:

t = s;                      // equivalent to...
t = (int *) s;              // equivalent to...
t = (void *) s;             // equivalent to...
t = (T) (int *) s;          // equivalent to...
t = (T) (void *) (int *) s;

These particular implicit conversions are called standard conversions, as they are required by the C++ standard and predefined by the compiler. You can also create user-defined conversions for your own class types. In

class S
    {
public:
    operator int();
    };

int f()
    {
    return S(); // implicit conversion from S to int
    }

the final return statement implicitly converts from S to int through the user-defined conversion S::operator int(). Similarly, the return statement in

class T
    {
public:
    T(int);
    };

T f()
    {
    return int(); // implicit conversion from int to T
    }

implicitly converts int to T with the user-defined conversion T::T(int).

If you combine these two:

class S
    {
public:
    operator int() const;
    };

class T
    {
public:
    T(int);
    };

T f()
    {
    return S(); // implicit S to int, implicit int to T
    }

the return statement requires two implicit user-defined conversions:

From S to int through S::operator int().
From int to T through T::T(int).
With a standard-conforming C++ implementation, this code won't work: the standard allows at most one implicit user-defined conversion on a single value. Visual C++ 6.0 incorrectly allows the two-conversion sequence anyway, without any clue something's amiss. Visual C++ .NET also allows it, but at least flags the sequence with a warning:

warning C4927: illegal conversion; more than one user-defined
conversion has been implicitly applied while calling the constructor
'T::T(int)'

If you tell Visual C++ .NET to treat this warning as an error (cl /we4927), then the compiler behaves according to the standard and won't accept the code.

To render the example standard conformant, make one of the implicit conversions explicit with either

return T(S()); // implicit S to int, explicit int to T

or

return int(S()); // explicit S to int, implicit int to T

-------------------


Member Initialization
Most static members must be initialized outside the class declaring them. This is true even for const members:

class X1
    {
    static double const d = 1; // error
    };

class X2
    {
    static double const d; // OK
    };

double const X2::d = 2; // OK

This truth can lead to awkward dichotomies between constants declared inside and outside of classes, and between function members defined inside and outside of the declaring classes:

int const n = 10;

class X
    {
public:
    static int const n; // X::n is declared but not initialized
    void f1()
        {
        int a[::n]; // OK, ::n's value is known
        }
    void f2()
        {
        int a[X::n]; // error, X::n's value isn't known yet
        }
    void f3();
    };

int const X::n = 10; // X::n is now initialized

void X::f3()
    {
    int a[X::n]; // OK, X::n's value is now known
    }

Whenever a member's behavior depends on where that member is defined, confusion and trouble often follow. To prevent this particular trouble, old-school C++ programmers often employ the enum trick:

// ...

class X
    {
public:
    /*static int const n;*/
    enum
        {
        n = 10
        };
    // ...
    void f2()
        {
        int a[X::n]; // now OK, X::n's value is known
        }
    // ...
    };

/*int const X::n = 10;*/

// ...

The enum trick is a first-class hack. It works, but at the cost of subverting the general intent of enumeration types. It is also limited to the underlying integer type implementing the enumeration, which may vary from compiler to compiler, or even among enumerations with the same compiler. Since you can't specify the underlying enumeration type, you can't directly control or even predict how X::n is treated when converted to that unknown type. (The Visual C++ Managed Extensions allow you to specify the underlying type of managed enumerations. I'm aware of no extension for specifying the type of unmanaged or native enumerations.)

Happily, you don't have to care about any of this anymore. Fairly late in the standardization game, the C++ standards committee patched the C++ standard to let static integer constants be initialized in their declarations:

class X
    {
public:
    static int const n = 10; // initialization OK
    void f2()
        {
        int a[X::n]; // OK
        }
    };

int const X::n = 10; // now an error -- multiple initialization
int const X::n;      // definition OK, but might not be required

Visual C++ .NET compiles this properly. Visual C++ 6.0 interprets the initialization of X::n as an illegal pure virtual function declaration.

The feature has restrictions:

Members must be static.
Members must be const.
Members must have some integral or enumeration type.
The initializing value must be an integral constant expression.
Members can be initialized at most once. If you initialize them within the class, you can't initialize them again in their definitions.


----------------

Scope
I discuss this at length in my previous column, so I won't belabor it here. In short, Visual C++ 6.0 does not properly scope declarations in a for statement when Microsoft-specific language extensions are enabled (cl /Ze):

for (int i;;)
    ;
i = 0; // OK, i is (incorrectly) still available here

Visual C++ .NET maintains the Visual C++ 6.0 behavior by default. But if you use the compiler option /Zc:forScope along with /Ze, Visual C++ .NET observes the standard-required behavior:

for (int i;;)
    ;
i = 0; // error, i is (correctly) no longer available

Note that both compiler versions maintain proper for scope when language extensions are disabled (cl /Za).

-----------

Return from Main
Many programmers reckon that all programs terminate through a return statement in main:

int main()
    {
    // ...
    return 0;
    }

But consider:

#include <cstdlib>

int main()
    {
    std::exit(1);
    return 0; // unreachable code
    }

or

int main()
    {
    throw 1;
    return 0; // unreachable code
    }

Clearly a program can terminate without actually encountering a return statement in main. The C++ standard accordingly allows main to be defined without any return statement at all, or with only some code paths executing a return. Where main returns normally without benefit of a return statement, the program acts as if main actually exited with return 0.

The small program

int main(int argc, char **argv)
    {
    if (argc == 1)
        return 1;  // exit point #1
    if (argc == 2) // exit point #2
        throw 2;
    }              // exit point #3

exits in one of three ways:

If argc is 1, the program explicitly returns 1 to the calling environment.
If argc is 2, the program returns nothing to the calling environment, but instead terminates with an exception.
If argc is any other value, the program implicitly returns 0 to the calling environment.
Visual C++ .NET gets this right, while Visual C++ 6.0 issues a warning about some code paths not returning a value.

On a related note: Visual C++ 6.0 allows

void main()
    {
    }

in violation of the C++ standard. Entirely too many C++ programmers grow up believing that main returning void is standard-conformant, simply because Microsoft's compilers allow it. I've lost count of how many times I've educated inexperienced programmers that just because Visual C++ 6.0 allows a feature doesn't mean the standard allows it.

I'm thrilled to report that Visual C++ .NET flags this same construct with:

warning C4326: return type of 'main' should be 'int' instead of 'void'

To properly treat this warning as an error, compile with cl -We4326.

---------------

Other Return Types
If a function in a derived class shares the same name and parameter list as a virtual function in a base class, then the derived-class function overrides the base-class function, and can be called polymorphically. If the names are the same, but the parameter lists differ or the base-class function is non-virtual, then the derived-class function hides the base-class function. To illustrate:

class Base
    {
public:
    virtual void f1();
    virtual void f2();
    };

class Derived : public Base
    {
public:
    virtual void f1();
    virtual void f2(int = 0);
    };

int main()
    {
    Derived d;
    Base &b = d;
    b.f1(); // OK, calls Derived::f1
    b.f2(); // OK, calls Base::f2
    }

Derived::f1 overrides Base::f1, since both parameters lists are the same. Conversely, Derived::b2 hides (but does not override) Base::f2—even though Base::f2 and Derived::f2 can be called with the same argument lists, their parameter lists are different.

The language rules once required that overriding functions always share return types:

class Base
    {
public:
    virtual void f1();
    virtual void f2();
    };

class Derived : public Base
    {
public:
    virtual int f1();    // error
    virtual int f2(int); // OK
    };

Derived::f1 properly overrides Base::f1 as before. Derived::f2 also overrides Base::f2; but because the return types differ, the compiler flags an error.

This restriction is nettlesome when you want the relationship between the return types to parallel the inheritance:

class Base
    {
public:
    virtual Base &clone() = 0;
    };

class Derived : public Base
    {
public:
    virtual Derived &clone();
    };

Because the older overriding rules didn't allow this, you had to resort to an unsatisfying compromise:

class Base
    {
public:
    virtual Base &clone() = 0;
    };

class Derived : public Base
    {
public:
    virtual Base &clone(); // override
    };

int main()
    {
    Derived d;
    Base    &b1 = d.clone(); // OK, but...
    Derived &d1 = d.clone(); // unintuitively an error
    }

To let such constructions work, the C++ committee changed the language rules. Overriding return types may now differ from each another if they relate by inheritance as well:

The return types either are both pointers or are both references—types that can engage in polymorphic behavior. Call the base-class function's possible return types B * and B &, and the derived-class function's D * and D &.
B is directly or indirectly an unambiguous and accessible base of D.
B * and D *, or B & and D &, have the same CV qualification. Additionally, D is at least as CV-qualified as B.
According to the standard, such return types are covariant with the polymorphic classes using them. Modifying my example in light of covariance yields

class Base
    {
public:
    virtual Base &clone() = 0;
    };

class Derived : public Base
    {
public:
    virtual Derived &clone(); // override
    };

int main()
    {
    Derived d;
    Base    &b1 = d.clone(); // OK, and...
    Derived &d1 = d.clone(); // OK!
    }

As you doubtless expect by now, Visual C++ 6.0 chokes on covariant types:

'Derived::clone' : overriding virtual function differs from
'Base::clone' only by return type or calling convention

while Visual C++ .NET accepts them.

Although covariant return types share an inheritance relationship with each other, they need not share that relationship with the classes using them:

class B
    {
    };

class D : public B
    {
    };

// ...

class Base
    {
    virtual B &f();
    };

class Derived : public Base
    {
    virtual D &f(); // override
    };

B and D are covariant with Base and Derived, even though the respective inheritance relationships are completely separated.

Covariance allows the structure among a family of classes to be paralleled in a family of return types. In effect, the return types—and the relationship among them—are a metaphor or mapping of the classes. While I understand the attractiveness and symmetry in this approach, I find covariance somewhat arbitrary. Consider what happens if you change clone to "return" through an outbound argument:

class Base
    {
public:
    virtual void clone(Base *&) = 0;
    };

class Derived : public Base
    {
public:
    virtual void clone(Derived *&); // hides Base::clone
    };

Because Derived::clone and Base::clone have different parameter lists, the former hides the latter. To make Derived::clone an override, you must go back to

class Base
    {
public:
    virtual void clone(Base *&) = 0;
    };

class Derived : public Base
    {
public:
    virtual void clone(Base *&); // overrides Base::clone
    };

which just resurrects the original problem:

Derived d, *dp;
Base *bp;
d.clone(bp); // OK, but...
d.clone(dp); // error

Covariance can't help you if you return values by argument—it works only for actual function return values. As I say, this strikes me as somewhat arbitrary.

(Before language lawyers descend: I understand that parameter covariance would likely complicate the grammar, if indeed such covariance could even be specified. I'm simply pointing out that covariance seems more of a special case than a general solution.)

---------

Exception Handling
Before the great Deep C++ hiatus, I wrote at disturbing length about C++ exceptions. As I noted then, Visual C++ 6.0 fails to support several exception-handling features required by the standard. While Visual C++ .NET still falls short, it does finally allow function try blocks. Such blocks solve a problem normal try blocks can't: handling exceptions thrown before a function's body is actually entered.

In the definition

class X
    {
public:
    X(unsigned);
private:
    char *p;
    };

X::X(unsigned n) : p(new char[n]) // what if this throws?
    {
    try
        {
        // ... constructor code
        }
    catch (...)
        {
        }
    }

any exception thrown by new char[n] would occur before the constructor body is entered, and thus couldn't be caught by the constructor's exception handler. Recasting the try block as a function try block:

X::X(unsigned n)
try
        : p(new char[n])
    {
    // ... constructor code
    }
catch (...)
    {
    }

allows exceptions from the entire function, including the member initializer, to be caught.

Visual C++ 6.0 is completely ignorant of function try blocks. In fact, it gets so confused that it issues the same diagnostic twice:

error C2143: syntax error : missing ';' before try
error C2143: syntax error : missing ';' before try

I discuss function try blocks at some length in this earlier column.

---------

Templates
Visual C++ .NET fixes a few conformance problems involving templates.

Non-Type Template Parameters
Visual C++ .NET finally allows one of my pet features—finding the length of an array during compilation:

#include <stdlib.h> // defines size_t

template<typename T, size_t N>
size_t lengthof(T (&)[N])
    {
    return N;
    }

int main()
    {
    int a[10];
    lengthof(a); // calls lengthof<int, 10>
    }

If you masochistically compile this program with Visual C++ 6.0, you'll experience wondrous diagnostics:

error C2265: '<Unknown>' : reference to a zero-sized array is illegal
error C2266: '<Unknown>' : reference to a non-constant bounded array
is illegal
error C2784: 'unsigned int __cdecl lengthof(T (&)[1])' : could not
deduce template argument for ' (&)[1]' from 'int [10]'

With the older compiler, the best approximation of a proper lengthof has been

#define LENGTHOF(a) \
    (sizeof(a) / sizeof(*(a)))

We all have our fetishes, and this has been one of mine. I've written about lengthof versus LENGTHOF at, well, length in my CUJ columns (particularly here).

----------

Template Template Parameters
Template parameters may also be templates themselves, or what the standard calls template template parameters:

template<typename T>
class my_node
    {
    // ...
private:
    T value;
    my_node<T> *next;
    };

template<typename Value, template<typename T> class Node>
class container
    {
    // ...
private:
    Node<Value> *list;
    };

container<int, my_node> x;

Node is a template template parameter of container; my_node is the corresponding template template argument in x's definition.
-----------------

Template Friends
According to the Visual C++ team, the new compiler supports friends in templates "to some degree." Here's an example adapted from one in the C++ standard:

template<typename T>
class C2;

template<typename T>
void f2();

template<typename T>
class X
    {
    template<typename T1>
        friend class C1;
    template<typename T1>
        friend void f1();
    friend class C2< X<T> >;
    friend void f2< X<T> >();
    friend class C3;
    friend void f3();
    };

This all actually compiles with Visual C++ .NET, and encompasses the gamut of friends: templates, template specializations, and ordinary (non-template) classes and functions.

Visual C++ 6.0 completely loses the plot while interpreting this code, and the error messages show it. The paragon of diagnostic lucidity comes on line 11, at the first friend declaration:

error C2059: syntax error : '<end Parse>'

----------------------
Template Specialization
Visual C++ .NET now prevents multiple definitions of the same template specialization, at least for function templates:

typedef int INT;

template <typename T>
void f();

template<>
void f<int>()
    {
    }

template<>
void f<INT>() // redefines f<int>
    {
    }

Visual C++ 6.0 accepts the code while ignoring the second definition. Visual C++ .NET properly rejects this code, although the diagnostic is a bit untidy:

error C2084: function 'void f<int>(void)' already has a body
see previous definition of 'f@?$@H'

No, that's not profanity. It's the function's mangled or decorated name, which probably isn't supposed to be there.
---------------

'KB > C/C++' 카테고리의 다른 글

C와 어셈블리 호출  (0) 2006.08.15
VC++.NET 새 키워드  (0) 2006.06.28
typeof  (0) 2006.06.28
code profiler  (0) 2006.04.17
함수 링크 순서 정해주기  (0) 2005.10.28

+ Recent posts