Some Notes about Mixed Types
20/7/2005 external link
Perhaps the most important reason to use C++ for managed code development is
that C++ fully understands managed code and existing C++ code. This positions
C++ as the ultimate language for doing managed-native interop programming. It is
worth sitting back and remembering just how important that capability is. This
allows you as the programmer to take advantage of any library that exists
whether it is a native library or a .NET library. At the same time, every work
item can be accomplished by using C++ alone. Although the CLR allows language
interop between assemblies and to some degree within an assembly, it's still
harder than using only one language.
C++ definitely makes interop easier than any other language on the .NET
platform, but it still requires effort... and intelligence. The most fundamental
thing to understand about .NET interop are differences between memory allocators.
These are the two models that must be understood: the CLR garbage collector and
traditional native heap memory managers.
Traditional native heap memory managers
Pick up any book on writing a memory manager, and you'll quickly learn how
tricky this can be. Nevertheless, when it comes down to the final steps of
squeezing out every last bit of performance, it inevitably comes down to writing
a custom memory manager. What is most common about traditional memory managers
is that the data stored in memory is never moved by the system. That is, from
the point the memory is allocated (via new, malloc, or your favorite memory
allocation function), the object stored in that memory can only be moved by the
program itself. The address range of that memory allocation is given over to the
program's control until the program releases that address range (via delete,
free, or your favorite memory deallocation function).
Even most garbage collectors follow this same behavior. That is from the
point of allocation, a memory range is under the control of the program until
the memory range is no longer referenced. At this point, a garbage collector can
free the memory. The contents of the data range are never stored elsewhere
unless the program itself moves the data.
Performance of memory allocators are measured in several ways, including the
time it takes to allocate memory, time it takes to free memory, wasted memory
used for padding between allocations, and long term working set. In many cases,
I've seen server applications run out of memory – not because there was a
memory leak, but because the heap's memory was fragmented. Memory fragmentation
means that while enough free memory exists for an allocation, not enough free
space is available in a contiguous block of memory addresses.
The CLR garbage collector
The garbage collector that comes with .NET is known as a generational,
compacting garbage collector. It's really quite sophisticated for the task of
memory management. Every time a garbage collection happens, any free space
between objects is compacted out by moving the remaining live objects to the
beginning of the heap. This means that free memory is always available in a
contiguous chunk in higher address range of the heap, which makes memory
allocation super fast.
There are a number of other characteristics that make the .NET garbage
collector world class, but they're not relevant to the eventual topic of this
discussion. What is relevant is that objects on the garbage collected heap are
moved by the system. To maintain consistency in the system, any memory address
that referenced the object is also updated by the system. Unfortunately, the
system (by which I mean the CLR) only knows about memory references that
originated from managed code (managed code is not necessarily equivalent to MSIL). This means that any reference to an
object on the garbage collected heap originating from unmanaged code will not be
updated, and thus the program could break after the garbage collector runs.
The best way to allow unmanaged code to refer to objects on the garbage
collected heap is to prevent the garbage collector from moving the object. The
feature that tells the garbage collector to leave an object alone is called
pinning. .NET actually provides several ways to pin an object, but there are
risks associated with all of them. I'll discuss this more later.
Choosing which memory allocator to use
Choosing whether to use the .NET garbage collector for an object verses using a
traditional memory allocator comes down to choosing whether to represent data
with a ref class or a native class. All ref classes take memory from the garbage
collected heap whenever they are instantiated with the MSIL newobj instruction.
All native classes take memory from the native heap, the execution stack, or
global memory.
In .NET, there is a category of types called value classes that are optimized
for efficient copying. As such, an instance of a value type can occupy memory
anywhere on the system with some restrictions. A value type can have a handle as
a member (see my discussion of the
handle
design in C++). Handles have to be visible to the CLR, so a value type that
contains a handle cannot be allocated on the native heap.
Now, we're at an interesting point in the discussion... with all these
restrictions for what memory allocator a type can be instantiated with, how do
we effectively interoperate between the two memory allocators. A typical
approach to coding is to make a type a member of another type. Let's try both
directions:
struct NativePoint {
int x, y;
};
ref struct ManagedPoint {
int x, y;
};
class C {
ManagedPoint mp;
};
ref class R {
NativePoint np;
};
If you try to compile this program, the Visual C++ 2005 compiler gives the
following errors:
t.cpp(5) : error C3265: cannot declare a managed 'mp' in an unmanaged 'C'
may not declare a global or static variable, or a member of a native type that
refers to objects in the gc heap
t.cpp(5) : error C3076: 'C::mp' : you cannot embed an instance of a reference
type, 'ManagedPoint', in a native type
t.cpp(9) : error C4368: cannot define 'np' as a member of managed 'R': mixed
types are not supported
As you can see, the compiler doesn't really like the code above. It doesn't
like putting the ManagedPoint in the native class C
that it even gives two errors. The third error brings up this notion of a
mixed type.
What is a mixed type?
In the language specification, I wrote the following definition for mixed types:
"A mixed type is either a native class or ref class that requires object
members, either by declaration or by inheritance, to be allocated on both the
garbage collected heap and the native heap." When writing a standard, I tend to
be terse since every word can end up being interpreted broadly. Here, I'll try
to explain the issue informally.
As I said earlier, native classes are always allocated on native heap or on
the execution stack, and likewise ref classes are always allocated on the
garbage collected heap. When the native class C tried to embed a ref class
ManagedPoint, the compiler doesn't have any place to put the object since parts
of it have to be on both the native heap and the garbage collected heap. The
same is true the other way with class R. Both native class C and ref class
R are
mixed types.
In the old syntax for writing managed code in C++, the compiler allowed a
particular kind of native class to be a member of a ref class. In C++, a class
that has no constructor, destructor, or copy semantics is called a POD type,
which stands for plain old data. It is equivalent to a C-structure. These
types can be safely copied around with memcpy which makes them more suitable to being
members of ref classes. Unfortunately, these types are used by unmanaged code
frequently, meaning that if the POD is allocated on the garbage collected heap,
the garbage collector cannot move that region of memory while unmanaged code is
accessing the object. This is accomplished with pinning. While there are several
ways to pin an object, the most accessible mechanism is a pinning pointer.
A pinning pointer works by keeping whatever it points to from moving during a
garbage collection. For a pinning pointer to be effective, it must be an active
local variable in a function somewhere on the call stack. As soon as a function
returns, a pinning pointer no longer keeps an object for moving. Given this
behavior, the C++ language design team observed this kind of code frequently.
struct Point {
int x, y;
};
ref struct R {
Point p;
};
void F(Point* ptr);
Point* G(R^ r) {
pin_ptr<Point> pinp = &r.p;
return pinp;
}
void K() {
R^ r = gcnew R;
F(G(r));
}
The function G has a terrible mistake – something we call
a "GC hole". Using the behavior from Visual C++ 2002 and Visual C++ 2003,
Point is a POD so it can be embedded in R. Thus, the memory for the
Point data is on the garbage collected heap. G pins the
object where the memory for the Point stored, and uses the pinning
pointer to get a native pointer to the Point. The coding error is
that G returns the native pointer and allows the pinning pointer to
become inactive. In effect, G returns a pointer to memory on the
garbage collected heap that is not pinned.
This coding mistake was so common that we were compelled to fix it. We tried
relentlessly to correct pin_ptr so that this kind of code could not
exist. Unfortunately, that requires data flow analysis which is an incomputable
problem without further constraints to the language. In the end, there was very
little we could to change pin_ptr. It became clear that allowing
PODs to be allocated on the garbage collected heap was an idea that could not
survive in the new language design.
How mixed types work in the new language design
While all the code examples thus far have compiler errors with Visual C++ 2005,
the C++ language design team wants the code to compile. In the beginning of 2003
we spent a few months writing a lengthy design paper that proposed "mixed types"
as the solution to the problems mentioned above. The proposal suggested that
whenever a mixed type occurred, the compiler would split all the "native parts"
and the "managed parts" into two pieces. The "native parts" would be allocated
on the native heap and the "managed parts" would be allocated on the garbage
collected heap. The two parts would then be connected to each other. All of this
splitting would be mostly invisible to the programmer, leaving the compiler to
do all the hard work.
We were so intrigued by the possibilities here that we went ahead and
designed ways to make it look like any native class could be allocated on the
garbage collected heap, or even get ways to have pointers to a ref class. All of
this came to be known as the C++ unified type system. The document describing
how all this works is a bit over fifty pages, and although my blog postings are
generally fairly lengthy, fifty pages is a bit too much even for my tastes.
Provided there is interest, I may spend time writing about the unified type
system over a series of articles.
Why native native classes, including PODs, should not be on the garbage
collected heap
In past releases, PODs could be on the garbage collected heap. When taking their
address, you get an interior pointer, which you would then have to pin in order to
get an unmanaged pointer. Since mixed types say that native classes are never
allocated on the garbage collected heap, we were able to assume that native
types never needed to be pinned. This assumption actually goes to the heart of
the type system. Consider this common implementation for the swap routine:
template<typename T>
void swap(T% t1, T% t2) {
T temp = t1;
t1 = t2;
t2 = temp;
}
We wanted to make it possible to replace a native reference (&) with a
tracking reference (%) without any trouble. After all, they are both pointers
underneath the covers and there isn't the same risk of leaking an unpinned
reference to unmanaged code. Simply substituting % for & works until a
conversion from an N% to an N& is necessary (in the language design, we
typically use the capital letter N to represent native classes). Consider this
simple code:
struct C {
int X;
C(int value) : X(value) {}
C(C& c) : X(c.X) {}
C& operator=(C& c) {
this->X = c.X;
return *this;
}
};
void K() {
C c1(1);
C c2(2);
swap(c1, c2);
}
Like every other native class in existing code, the copy constructor and copy
assignment operator use native references for the argument. The call with the
swap template could not succeed unless C either provided copy
functions that took tracking references or we allowed an N% to
convert to an N&. We chose to allow the conversion which was based
on the assumption that native classes would never be on the garbage collected
heap.
For the most part, all existing STL algorithms can be made to work for
managed code by simply replacing & with %. If managed
classes exposed iterators, the STL algorithms can work.
Why does Visual C++ 2005 not support mixed types?
Basically, if we had implemented everything written in the unified type system document, we would be
calling this release Visual C++ 2010. Really! We designed some pretty advanced
and complex stuff. The compiler team figured it would be more useful to release
a product sooner that provided the core language and leave out parts that could
be worked around with libraries. Just getting the fundamentals done with high
quality for Visual
C++ 2005 was a daunting task.
Going forward, we will be prioritizing work based on customer feedback and
scenarios that are blocked. Just listing out high priority scenarios for
upcoming releases could take a while, so I'll once again avoid exploring that
topic here. Regarding feedback, we are already getting
feedback that allowing PODs as members of ref classes is important. We also
received the same feedback from teams inside Microsoft who were trying to
program with the new C++ features.
Several times, we tried to narrow the set of features needed to implement
some part of mixed types. Ultimately, we ended up getting a very narrow subset of the mixed type
proposal that would work. Unfortunately, changes to the compiler in the past
half-year needed to be low risk (meaning that changes were certain to not cause
a slip to our November 7th delivery date). So far, we have been unable to mitigate the
risks involved in implementing even the simplest approach to mixed types.
How to workaround absence of mixed types
Given that Visual C++ 2005 gives errors for mixed types, the next best approach is a library. First, I'll address the case where a library solution has
existed for a while. As discussed earlier, the compiler returns errors C3265 and
C3076 when a program tries to embed a ref class inside a native class. The
gcroot template has been around for a while, which allows a native class to
effectively hold onto a handle to ref class. It is used as follows:
#include <vcclr.h>
ref struct ManagedPoint {
int x, y;
};
class C {
gcroot<ManagedPoint^> mp;
public:
C() : mp(gcnew ManagedPoint) {}
~C() { delete mp; }
};
The gcroot template doesn't make embedded semantics the default –
the ref class object still has to be created with gcnew. It's good
enough to be productive though, and there are already further derivations of
gcroot included in the libraries with Visual C++, such as
auto_gcroot.
So, putting a ref class inside of a native class is solved with gcroot. What
about putting a native class in a ref class? Today, there is no library included
with Visual C++ that solves this. So... let's make one here.
The gcroot template is a wrapper for the
System::Runtime::InteropServices::GCHandle class. That's how data on the native
heap can get a reference to an object on the garbage collected heap. Going the
other way around is much easier – a pointer will do. Here is the first
rewriting of some of earlier code:
// First attempt at embedding a native class
// inside a ref class
struct NativePoint {
int x, y;
};
ref class R {
NativePoint* np;
public:
R() : np(new NativePoint) {}
~R() { delete np; }
};
The code above will certainly work in many cases, but it subject to memory
leaks when the class R is cleaned up by the garbage collector. This happens when
an instance of R is not cleaned up with a destructor, but is instead lets the
garbage collector call the finalizer. To fix that, we'll add a finalizer to the
code before:
// Second attempt at embedding a native class
// inside a ref class
struct NativePoint {
int x, y;
};
ref class R {
NativePoint* np;
public:
R() : np(new NativePoint) {}
~R() { this->!R(); }
!R() { delete np; }
};
Notice that I'm avoiding code duplication by calling R's
finalizer from R's destructor. This pattern certainly gets us
closer to working around the absence of mixed types. Nevertheless, it's clear
that as R holds onto more and more resources, maintaining the
destructor and finalizer, especially to be exception safe, will become unwieldy.
We also need to consider the very real possibility that a class might be
finalized more than once, or finalized when the constructor didn't complete
execution. To solve these problems, we're going to put the resource management
code in a separate template class:
// Third attempt at embedding a native class
// inside a ref class
template<typename T>
ref class Embedded {
T* t;
!Embedded() {
if (t != nullptr) {
delete t;
t = nullptr;
}
}
~Embedded() {
this->!Embedded();
}
public:
Embedded() : t(new T) {}
static T* operator&(Embedded% e) { return e.t; }
static T* operator->(Embedded% e) { return e.t; }
};
struct NativePoint {
int x, y;
};
ref class R {
Embedded<NativePoint> np;
};
With the Embedded template, we've finally got a library solution
for embedding native classes in a ref class. Of course, the illusion isn't
complete given that member access to the embedded native class must use the
arrow (->) operator instead of the dot (.) operator. I
think we can live with that difference until Visual C++ does introduce some
support for mixed types.
You'll note that this template is different from auto
pointer like templates in that Embedded completely owns the management of the native class –
it never gives up that ownership. The native class is created when the Embedded
class is created, and the native class is cleaned up when the Embedded class is
cleaned up.
Kenny
Kerr wrote up some notes about this same subject in
which he provided an AutoPtr template where the ownership of the resource is
more in control of the programmer using the AutoPtr template. In many ways the
AutoPtr
template is more flexible for non-POD classes as it allows for use of a non-default constructor.
AutoPtr really needs to use a finalizer though (otherwise it's
likely to leak resources). J
The last area we have so far left uninvestigated is the ability to embed
native arrays in a ref class. Previous versions of Visual C++ allowed this to
happen if the element type of the native array was a POD type, fundamental type, or a simple
value class (in other words a class that has no handles). A native array is
accessed via pointers and even is synonymous with pointers in much of the C++
type system. For all the reasons listed earlier, native arrays can no longer be
allocated on the garbage collected heap. This brings up an interesting problem –
holding onto a pointer to a native array from a ref class adds an extra level of
indirection. In some cases that can impact performance. In fact, some
languages introduced the idea of a fixed array just to solve this problem. So,
how can C++ get the same behavior given the constraints above?
C++ does let you put fundamental types and simple value classes as members of
a value class, and the CLR provides a mechanism for explicitly stating the size
of a value class. Putting that together, we have a solution –
the inline_array template written by Mark Hall and improved upon by Shaun
Miller:
template<typename T, int size>
[System::Runtime::CompilerServices::UnsafeValueType]
[System::Runtime::InteropServices::StructLayout
(
System::Runtime::InteropServices::LayoutKind::Explicit,
Size=(sizeof(T)*size)
)
]
public value struct inline_array {
private:
[System::Runtime::InteropServices::FieldOffset(0)]
T dummy_item;
public:
T% operator[](int index) {
return *((&dummy_item)+index);
}
static operator interior_ptr<T>(inline_array<T,size>% ia) {
return &ia.dummy_item;
}
};
ref class R {
inline_array<int, 10> arr;
};
This of course allows you to really embed an array of fundamental types.
Interestingly, the generated MSIL and metadata using the inline_array
template is almost exactly the same thing generated by fixed arrays in C#. It's
great to see workable library solutions show up before having to extend the
language.
To wrap up...
I know this subject requires a lot of knowledge to grok, but it does come up a
surprising amount. Most programmers moving code from previous releases of Visual
C++ to the new syntax introduced in Visual C++ 2005 are going to see these
issues the most. The fastest way to get a POD in a ref class is to use the
Embedded template.
We will be listening to customers to determine where to go next. Providing at
least some mixed type support seems necessary, but if you have anything to
say... please speak up! Sending feedback and suggestions through the
MSDN Product Feedback Center
is a great way of getting a permanent record of the request (and we really do
spend time with feedback sent there). I'm always interested in feedback too, so
don't hesitate to send notes my way.
Compiler Switch Changes in Visual C++ 2005
9/7/2005 external link
One noticeable change in the coming release of the Visual C++ compiler is the
changes to compiler switches. If you use the project system, when the IDE
upgrades your project files, many of these changes will be taken care of
automatically. However, if you use another build system like nmake, you might
have to fix some build issues.
Why did we remove and deprecate some compiler switches?
The Visual C++ compiler has been around a long time. Long enough that many
features that were once very useful are no longer needed (optimizations for the
486 probably don't get used that much these days). In some cases, new switches
have been added that mean exactly the same thing as an existing switch.
Furthermore, some switches existed that are essentially evil and easily led to
the program errors.
Much of the investigation here began when we started thinking harder about
security issues during the development of Visual C++ 2003. In particular, as
soon as potential security issues became a higher priority than backwards
compatibility, we had to justify more of the feature set Visual C++ exposed and
of course we had to question how much testing we actually did on all the
configurations the compiler could exercise. The first change was modest. Visual
C++ 2003 deprecated the Gf switch. In that compiler, you would have seen the
following warning:
c1xx : warning C4349: /Gf is deprecated and will not be supported in future
versions of Visual C++; remove /Gf or use /GF instead
For everyone who does not immediately recall what the Gf
switch does, it pooled all the string literals into a writable page of memory.
Just the description screams of security issues if not simple reliability
issues. The alternative GF switch pools string literals into a
read-only page of memory, which will break C programs that mutate string
literals. Of course, pooling is a great way to reduce the size of a binary.
Goals behind the switch reduction
The changes to the switch model for Visual C++ 2005 came to be known as
switch reduction. It was designed along with another improvement, warning
families, that did not make it into this release of Visual C++.
Hopefully, warning families will make its way into the next release. But I'll
delay discussion of that since we're considering switches right now.
When I started to write the specification for the changes in this area, I
listed the following four requirements:
Removing the switch would benefit testing, by reducing the test matrix for the
compiler.
Removing the switch would lead to a better user experience.
Changing switches would make multiple versions and platforms have
consistent switches.
After considering the above, removing or changing the switch does not place
undue burden on users.
I also noted in the specification that changes to switches needed to
happen swiftly to avoid prolonged impact on users. Unfortunately, making changes
to compiler switches turned out to be harder than I ever imagined. Fixing all of
the files in the test harness took the majority of our effort. As a result, all
of the work called for in the switch reduction specification wasn't completed.
When it came to improved user experience, the focus was on making it easy for
programmers to select the correct defaults for their program or at least to easily
understand the interactions switches have with each other. A good example of
this are the Os and Ot switches, which mean "favor code
space" and "favor code speed" respectively. Neither of these switches do
anything unless the Og switch is also given to the compiler. This
issue frequently comes up. We can either fix it by warning the user that he
probably did something wrong, or we can make the switch model intuitive. Given
the overall complexity of just getting a standard C++ program to run correctly,
I favor making the compiler more intuitive and making it work by default.
The third requirement addresses the problem with the G3,
G4,
G5, G6, and G7 compiler switches. In a good
attempt to allow application writers to make use of the latest and greatest
hardware, nearly every compiler introduced a new better switch to make your
code even faster. Unfortunately, compiler switches end up in make files that rarely
get revised. It wasn't uncommon to see a make file specify a G4 or
G5 switch even though 80486 and Pentium have long been out of mainstream
production. The G-series of switches do not prevent programs from running on older
hardware, which was a common misconception. Eventually, Visual C++ just ignored the
G3, G4, and G5 switches and the program
compiled as if G6 had been given to the compiler. I'll say more on what
changed with these switches below.
Choosing what to do and getting it done
Throughout this process, I acquired a reputation for not liking switches. A
running joke in the Visual C++ product team was to suggest adding a switch to
me. To be honest, the reputation wasn't quite deserved. I just challenged the
notion of adding a switch with its necessity. To many times, switches were added
to the compiler to cover over other problems. In fact, that's par for the
industry. Most compilers offer dozens of switches to disable particular
optimizations because they break code. That's lazy – the
programmer using the compiler has to understand the impact of an optimization
and even if he does, he's likely to overlook something. The Visual C++
team has a much higher aspiration to make the compiler useful to every
application and systems programmer on Windows, not just geniuses with intimate
knowledge of every version of the compiler.
All that said, I started evaluating changes by doing none else than reading
the source code for the compiler driver. Through that I came across obsolete,
outdated, bizarre, undocumented, and useless switches. I looked at each one
asking whether it was necessary for the compiler in the long term and evaluating
each switch against the requirements listed above. I actually spent most of my
time trying to figure out what each switch did. Even asking developers who work
on the compiler, I'd sometimes get several different answers. In a few cases, no
one knew what the switch did. If our own team couldn't recall a switch's purpose,
it's not hard to believe nearly every programmer using Visual C++ will have the
same problem.
Out of this process came the first list of changes to the switch model. Team
members were given opportunity to comment along with the Visual C++ MVPs.
Through much discussion, the list was narrowed to what we originally
implemented. Early usage of the compiler in alphas and the first beta yielded feedback
that made us reconsider some of the changes. In several cases we reverted
changes due to direct feedback.
Before I go into more detail about some of the specific changes, a primary
concern was ease of migration. The most noticeable impact of a change like this
is breaking a build. Fortunately, additional warnings about compiler switches
usually come from the compiler driver cl.exe instead of the compiler itself (one
of c1.dll, c1xx.dll, or c2.dll). This is usually noticeable
by the message
number, Dxxxx instead of Cxxxx. As a result, the
WX switch does not cause a build failure when the driver warns about an
unrecognized or deprecated switch.
Now let's look at some of the specific changes...
Optimization switches
As I mentioned earlier, the G-series of switches offered two problem points.
First, many customers confused the meaning of these switches, thinking that it
was necessary to throw G4 to make the compiler produce code that
could still run on a 486 computer. Second, the switches tended to stay in make
files well past the intended period of time. In recent versions of Visual C++,
the G3, G4, and G5 switches were just
ignored.
Since G6 and G7 were the only two that made a
difference, the natural question was what the difference between these two were.
It turns out there was very little difference, and there were ways to generate
code that resulted in great performance on both Pentium III and and all the new
processors coming from both Intel and AMD. The switches weren't really necessary.
So, we removed them and reduced the testing matrix at the same time.
So far I've taken an x86 focused view of the world. On IA64, there are
significant differences between first generation processors and the following
generations. IA64 compilers continue to have the G1 and G2
switch because the usability problems haven't shown up on IA64, partly because
the market of developers is relatively tiny compared to x86.
On x64 platforms though, we tried at first to avoid generating different code
between compilers. In the end, we were compelled to support specific processors.
To avoid any confusion, rather than continue with the G-series of switches, the
x64 compiler has the favor:AMD64, favor:Pentium4,
favor:EM64T, and favor:blend
switches. It should now be clear that throwing one of these switches still
allows the program to run on other processors, but likely with weaker
performance.
Certainly, I would have preferred to see the same switch set on all
compilers. That hasn't happened yet, and it hasn't been necessary to drive that
idea to the top of my priorities.
Regarding other optimization switches, I did a cognitive walkthrough of the
optimization settings shown in the cl /? results. These were some of my
thoughts:
Ox and O2 are almost identical. They differ only in the fact that
O2
also throws GF and Gy. There is almost no reason to avoid throwing these two
switches.
It is difficult to know that Os and Ot are ineffective.
Using Og means that Oy Ob2 and
Oi are not thrown, which is usually only
useful for debugging purposes – it is not the starting point for a release
build, and it's better to use pragmas for this level of control rather than
changing the build system.
At the time, I wanted to converge the optimization settings so that multiple
switches resulted in the same behavior. This would allow build scripts to
continue working without changes, but would both reduce the test matrix and
improve default compiler switch selection. This is what I had proposed
converging to:
Os: now turns on optimization, favoring size (also throws Oy
Ob2 GF)
Ot: now turns on optimization, favoring speed (also throws Oi
Oy Ob2 GF)
O1: has exactly the same meaning as Os
O2: has exactly the same meaning as Ot
Od: disables optimization (also throws Oi- Oy-
Ob0 GF-)
Ox: has exactly the same meaning as Ot
Remove the Og switch
If you're using Beta 2 of Visual C++ 2005, you'll see that this convergence
of optimization switches did not happen. That's mostly because we didn't have
enough time in the schedule to do so. Despite known usability issues with
optimization switches, we don't currently have plans to complete this part of
the switch reduction specification in the next release.
Buggy features
It may come as a shock, but some of the features we've released in Visual C++
haven't had the level of quality that we wish it would have. The most notable of
all is automatic precompiled header (PCH) files. The YX switch was
used to tell the compiler to automatically select and create a PCH file.
Developers would use this to speed up a build, but in practice it slowed the
build down. Only in a few test cases did it benefit the build time. Given that
information, we decided to remove the switch. If you do want to use a PCH, the
Yc and
Yu switch still exist and, when used correctly, they will dramatically
improve build times.
There were a few other undocumented switches left over from experiments that
never showed results. We took the opportunity to at least deprecate these
switches.
I also listed the Wp64 switch to be deprecated and turned on by
default, which doesn't appear to have happened. Overall, the Wp64
switch is no longer necessary. Visual Studio 2005 now includes 64-bit compilers.
Compiling code with a 64-bit compiler yields accurate warnings and errors,
whereas the Wp64 option only yielded approximations and in many
cases had false positives and false negatives.
CLR switches
Visual C++ 2005 introduces at least three new switches for CLR modes. All told,
we have the following CLR modes:
clr-: This corresponds to not using CLR functionality at all. It
is the default.
clr: This tells the compiler to enable CLR
funcationality, using the new syntax, and to produce a mixed executable image
(one that can contain both machine code and MSIL). Object files generated from
this mode can be linked with object files compiled with the clr-
mode.
clr:oldSyntax: This tells the compiler to enable CLR functionality,
using the old managed syntax, and to produce a mixed executable image.
clr:pure: This tells the compiler to enable CLR functionality and to
produce a pure executable image (one that contains only MSIL).
clr:safe: This tells the compiler to enable CLR
functionality, to produce a pure executable image, and to only allow
verifiable source code through the compiler.
I'd say selecting the names for these switches was the hardest part of the
process. I heard concerns that "old syntax" could be insulting to
geriatric constituents. That was perhaps the highlight of the conversation.
The "safe" switch had equally heated debate as concerns would be raised
that Standard C++ isn't safe. The choice of "clr:safe" was for
consistency with other .NET languages, which typically have an "unsafe" switch.
"pure" has similar subjective concerns. Of course, it's too late to change the
names now... we'll all have to live with the ones listed above.
Each of these switches override each other, so it's not possible to mix these
modes (for example, old syntax and verifiable code cannot be mixed). In previous
releases, there was also the clr:noAssembly switch. In reality,
that should have been a linker switch since the compiler generates object files,
not assemblies. Thus, in Visual C++ 2005, the LN switch replaces
clr:noAssembly.
The other CLR switch, clr:initialAppDomain, was originally
scheduled to be removed from Visual C++ 2005 since it was there for
compatibility with CLR 1.0. It turns out that there are actually useful things
one can do with this switch with CLR 2.0. We discovered that late in the feedback cycle, so
the switch was left alone. Ideally, it would have been replaced with a switch
starting with G since it affects code generation.
The introduction of three new modes does add testing burden as the
matrix of test combinations now quadruples. In reality, we needed to focus
testing on all the CLR modes to bring it up to the same level of quality as
unmanaged code. One of the first things we noticed was that C code compiled with
any CLR option made very little sense. C doesn't have namespaces which makes it
obviously unable to access any of the .NET Frameworks. Compiling C code with the
clr switch was nothing more than an interesting experiment... an
experiment that unnecessarily expanded the test matrix. Thus, we made Tc
and TC conflict with all of the CLR modes.
Conformance switches and default behavior
After the work in Visual C++ 2003 to support more Standard C++ features, a common
complaint was that standard conforming code that should compile and run did not. It almost always
came down to a particular switch was not thrown. To that effect, there were
semantic differences between strict compliance mode Za and the
extensions mode Ze. I'm a strong believer that different modes in a
compiler should at most add and remove features from the language
– they should never change the semantics of the language.
Here are examples of where that principle is violated in Visual C++ 2003:
Compiling with and without Zc:wchar_t changes the meaning
of the wchar_t type, which impacts overload resolution, name
decoration, and binary compatibility.
Compiling with and without Zc:forScope changes the meaning of
the iterator variable in a for loop.
Compiling with GX- verses EHs determines
whether stack allocated C++ objects will have their destructors called in
the event of an exception.
Compiling with and without J changes the meaning of char.
Compiling with vd0 instead of vd1 breaks
binary compatibility and changes the capabilities of virtual inheritance.
Even when we were almost doing the right thing, as in the case of GR
(enable dynamic type info), Visual C++ had the wrong default. In the past, the
compiler defaulted to no dynamic type info. The result of all of this is that
Visual C++ wasn't standards conformant out of the box. You had to set a handful
of compiler switches to get top notch conformance with the C++ standard.
Unfortunately, whenever you set all these switches, many libraries no longer
compiled.
So, while Visual C++ 2005 hasn't been on the bleeding edge of adding dozens
of new standard features, we have been getting more and more libraries to
compile cleanly with a mostly conformant mode. Visual C++ 2005 now makes the
following switches on by default: GR, Zc:wchar_t,
Zc:forScope, fp:precise, and GS. The one
switch I wish we had made a defult but we didn't get to was EHsc
(enable conformant exception handling). Because some new defaults certainly
break code, new switches were added to override them, including GS-,
Zc:wchar_t-, and Zc:forScope-. Some of these new
switches are deprecated off the bat since you are better off fixing code than
just ignoring problems.
At the same time, I have problems with the Za switch. At first,
it makes some sense, but very few people actually write the entire program in
the restrictive subset that is Standard C++. More often, they want parts of the
program like the core engine to be standard compliant. Just as using Wp64
was a bad solution to diagnose portability of code between 32-bit and 64-bit,
using Za to diagnose portability between Visual C++ and other
compilers is a truly bad model. If you care about portability, you'll compile
your code with multiple compilers on a regular basis. The Ze switch
turns out to be completely unnecessary because it is the default mode of the
compiler and it is not possible to override the Za switch. Thus,
the Ze switch is deprecated in Visual C++ 2005. In the long term, I
hope that the default mode will be able to compile any code that Za
can with exactly the same behavior, thus making Za unnecessary. For
ease of diagnosing non-standard extensions, the warning families feature I spoke
about earlier is a far better solution than the restrictive Za
mode.
Truly evil switches
As I mentioned in the introduction, the Gf switch is particularly
bad since it pools string literals into a writable section of memory. C
programs are allowed to mutate string literals, so a program that pools two
unrelated string literals can have unexpected results when a string is mutated.
Visual C++ 2003 deprecated this option, and it is now removed from Visual C++
2005. C++ isn't affected by this change to the same degree that C is because C++
specifies that string literals shall not be mutated, and thus the compiler
always allocates them in read-only memory.
Another switch that just should never be used is H. This switch
placed a maximum length on external names, which was achieved by just truncating
the names. The result is that multiple entities could have the same name. Even
more, undecorating the names in the debugger was impossible. Really, nothing
good could come out of using the H compiler switch, so it was
deprecated.
Yet another example are the Oa and Ow switches.
These switches gave the compiler the freedom to make assumptions about memory
aliasing that were often untrue. As a result, the optimizer made an optimization
that broke programs in subtle ways. Very few programs could actually work with
these options, so they were both removed from Visual C++ 2005.
Switches that had customer feedback
Originally, we deprecated the J switch. The switch changes the meaning of
char to mean unsigned char instead of signed
char. A program that always specifies signed or
unsigned in front of char would never be affected by this compiler switch; however,
nearly no program does. System headers should be vigilant and always specify
whether char is signed or unsigned in source code. That again is
where warning families can provide a solution. Anyways, we deprecated the switch
understanding that is probably could never be removed (it can be useful in
migrating source code from other operating systems). After a while, the feedback
on this one compiler switch became so overwhelming that we're leaving J
alone.
Another set of switches that had similar, if not thundering feedback, are the
vd switches. These control the binary layout for objects to enable
virtual inheritance. I personally find these switches quite distasteful because
it prevents code from interoperating, but we had feedback that a particular
virtual inheritance scenario didn't work. The only solution given the binary
format we had was to introduce the vd2 compiler switch. So, in my
opinion, the situation got even worse, but I don't see a better solution. C'est
la vie.
Switches that had replacements
In several cases, switches have been replaced by a group of switches. A good
example is the GX switch which ended up being replaced by
EHsc when all the EH switches were added. GX
stuck around for a while, but it is finally deprecated in Visual C++ 2005.
Another case of this happening are the floating-point switches. In the past,
Visual C++ used the Op switch to limit the freedom the optimizer
had with floating-point code generation. Now that Visual C++ 2005 has much more
granular control over floating-point with the fp:precise,
fp:strict, and fp:fast switches, Op was no
longer necessary. Thus it was removed from Visual C++ 2005.
Switches that were obsolete
Some switches have just been around so long that they cannot possibly be useful
anymore. One such switch is the G3 switch, as favoring 386
processors with the latest version of Visual C++ isn't going to happen. Another
example is the QIfdiv switch... processors that don't have the
Pentium division bug have long been dominant in the market. Not to mention, the
GM switch is no longer necessary, since enabling MMX instructions
in the compiler is no longer useful these days. A long forgotten switch from the
Windows perspective was GD which enabled specific optimizations for
DLLs. It turns out those optimizations weren't limited to DLLs, and the switch
has been unnecessary for years.
Bizarre switches
Sometimes problems are over-designed and under-implemented. One example of this
is the nologo- switch. It's meant to force the product banner to
show. Different versions of the product ignore it though.
Some other bizarre switches are the TO and To
switches, which tell the compiler to consider unknown files as object files. It
would be better to pass these files to the linker rather than the compiler,
especially if compiling with the c switch. These are deprecated in
Visual C++ 2005.
Another switch that is both bizarre and wrong is one that we should have
gotten rid of years ago. The switch remained undocumented for a reason
– only "bozos" would use it. In fact the name of the
switch meant bozo alignment, which kept the alignment of local variables the
same as if it had been declared in a class; basically preventing the
optimizer from reordering local variables. Fortunately, we were able to correct
all the code that used bozo alignment and removed the switch from Visual C++
2005.
The single-threaded CRT
Reducing the test matrix is really important to Visual C++. In the past, it has
taken more than six weeks to complete a full test pass. In Visual C++ 2005, we
haven't really made huge cuts to functionality (i.e. the test matrix hasn't
gotten any smaller), but we've made a few attempts. One such attempt is removing
the single-threaded CRT. As a result, the ML and MLd
switches needed to be removed.
The output of cl /?
Mostly command-line users would be impacted by the results of cl /?, but they're
more likely to use that to guide switch selection. Many of the changes I wanted
to see happen in the help results didn't actually happen. It's too bad, but I've
been busy driving the language design effort. First, I wanted to make the
results shorter and leave a more complete listing for the official
documentation. That would at least leave the really necessary switches (and
better defaults) for command-line users.
Second, I wanted to fix some of the text. If you read the description of the
GA switch, it says "optimize for Windows Application". Who wouldn't want to do
that? After all, Visual C++ only targets Windows. Well, it actually impacts
thread-local storage and the switch is safe for EXEs and not DLLs. I would have
replaced the description with something like "generate
TLS accesses for EXEs".
In the long term, many people have suggested having more in depth help
available at the command-line. Now that Visual C++ has support for localizable
text on the command-line, that's a more likely reality. Much will depend on the
feedback customers gives us over the coming years.
Conclusion
I started writing this because the first comment I got on my last post asked
about the G5, G6, and G7 switches. I certainly ended up writing much more, and
the sad part is that I could write way more than I already have. The experience
of investigating compiler modes and implementing the changes that actually did
happen was a huge learning experience for me. I'm still learning about customer
experiences due to changes like this. As I learn more and the rest of the Visual
C++ team learns more, we'll make even better design decisions in upcoming
releases. So, if you do have feedback, send it my way. J
Four years and still going
26/6/2005 external link
It has been a while since I have written here. Looking at the last time I wrote
an article of significance, I mentioned that I had insomnia. Indeed, I wrote
everything between midnight and sunrise. Fortunately for me, I haven't had
insomnia for the last year and half. This blog has suffered as a result.
So,
why am I writing now? Well, today marks my fourth year anniversary of working at
Microsoft as a full-time employee. It is a great moment for reflection,
evaluating accomplishments, and identifying missed opportunities. No doubt, going
from knowing almost nothing about C++ three-and-a-half years ago to writing the
entire specification for all the new language features in Visual C++ 2005 is
a huge accomplishment. It is such a monumental task that I wonder if it will ever
end.
This past week has been a time of nostalgia. Reviewing my experiences at
Microsoft as an intern and now, I am filled with hope and excitement. When I
worked as an intern in the Outlook team, it seemed as if so many great things
were about to happen in the software industry. Sure enough, great things are
happening... they're just taking an incredibly long time. After four years, I've
gained much more appreciation for the amount of effort it takes to change the
world. I expect to take all the lessons I learned over the past four years and
apply them to making an even greater difference in the next four.
Beyond the reminiscing, I regret not having said anything here for so long.
It is not because I have a shortage of things to discuss. To all those who asked me to write again (there are too many
to list here), I am making a commitment towards spending more of my
non-insomnia induced waking hours to sharing what I know. In no particular order, I am going to write about the following:
Design considerations for arrays
How to write an inline array in a ref class
Design considerations for overriding
Design considerations for for each
Design considerations for tracking references (and non-const binding to
temporaries)
Design considerations for strings
Design considerations for mixed types
Design considerations for class definitions
Comparing accessibility with visibility
Comparing hidebyname with hidebysig rules in C++
Design considerations for keywords
Design considerations for initonly and literal
Design considerations for delegates
Design considerations for properties and events
How to write a non-trivial event
Design considerations for conversions
Design considerations for safe cast and verifiability
Design considerations for delegating constructors
Design considerations for destructors and finalizers
Design considerations for boxing
Design considerations for enums
Design considerations for attributes
Design considerations for generics
XML documentation comments
Why we removed compiler switches in Visual C++ 2005
Thoughts about IntelliSense
Unicode support in the compiler
Compiler support for traits
Thoughts about reaching 100% conformance with the ISO C++ Standard
Discussion of some of the C++ breaking changes done for conformance
Design considerations for equality
Design considerations for Boolean conversions
Variadic macros
Managed calling conventions
How /clr:pure evolved
__declspec(process) variables in pure
[STAThread] and the main function
What is <MarshalCopy> and <MarshalDestroy>?
Design considerations for const and volatile in ref and value classes
Thoughts about Pre-JIT compilation verses JIT compilation
Thoughts about concurrent programming
If there are topics missing from this list or a particular topic should take
priority, please comment to let me know. To a large degree, the list above
represents subjects that I have become expert on over the last three years. With
four years past, I am looking at the challenges coming in the upcoming years.
I'm certain unexpected excitement and hard work lies ahead.
Templates and Generics
19/11/2003 external link
Insomnia and being a workaholic is an interesting
combination. It is amazing how much work can be accomplished in the eight hours
before everyone else comes to work. J
Anyways, I spent some time working on specifying generics
in C++ yesterday so I figured I'd write about that today. Perhaps the most
important message regarding generics is that they are not templates. That is
evident in the C++ language design as it supports both generics and templates.
At the PDC, I heard comments such as "generics are templates done right". This,
sadly, is a misinformed opinion that too many people share.
Generics and templates have a minor overlap in
functionality. They both make it possible to create parameterized types which
make it possible to create type safe collections. That's where the similarity
stops. First, let's look at some features templates allow and how they are
interpreted. I'll assume that you know the syntax for templates.
Templates are instantiated at compile-time with the
source code.
Templates are type safe.
Templates allow user-defined specialization.
Templates allow non-type parameters.
Templates use "lazy structural constraints".
Templates support mix-ins.
Templates are instantiated at compile-time. This is
huge. Anyone who really wants to understand the limitations of either generics
or templates needs to know this. This means that the same template instantiated
in two different assemblies actually have different types. The CLR includes the
strong-name of the assembly in the type, and thus [A]vector<int>
is different from [B]vector<int> even if they
were instantiated from the same template. A template is always emitted to an
assembly after it has been specialized. So, really a template disappears after
compilation. It is not possible to instantiate a template from another assembly.
Rather, to instantiate a template, the programmer needs the source code for the
template.
Templates are type safe. Templates were designed to
replace what many people were using macros for. Templates are fully type checked
by the compiler. In no way is there any textual substitution or macro-like
behavior in templates. Templates are indeed verifiable as long as the
implementation of the template does not use unverifiable features.
Templates allow user-defined specialization. First,
let me explain the difference between specializations and instantiations.
Consider the following code:
Collection<int> a;
Collection<int> b;
Collection<double> c;
Collection<X> d;
Here, there are four instantiations of a template but only
three specializations. The first two variables share the same specialization.
When defining a template, every template has a "primary template". This is the
most general template that the compiler will use unless there is a better
explicit specialization or partial specialization of the template. The
usefulness of user-defined specializations cannot be understated – it allows the
programmer to choose a different implementation for different template
arguments. For instance, if templates were used in a math library, this allows
the programmer to create different implementations for integer and
floating-point calculations.
Without user-defined specializations, the compiler creates
all specializations from the primary template. These specializations can still
generate fairly different code from each other. For instance, one specialization
may inline all function calls involving template parameters. Another
specialization may not inline the same function calls.
Templates allow non-type parameters. Non-type
parameter like integers or template template parameters allow templates to have
significant expressive power. Constant values in specializations are known by
the optimizer and therefore can be passed into the code via a number of data
flow analyses such as constant propagation and copy propagation. The resulting
code is far more efficient than one that must rely on accessing variables.
The combination of specialization and non-type parameters have actually
enabled an entire programming paradigm in C++ known as template
meta-programming. While it is entirely possible to go overboard with the
capabilities templates afford, there are numerous useful techniques.
Templates use "lazy structural constraints". What
happens when a template relies on a function and it's not there? When writing
the template, the developer can call member functions on the template parameter,
use operators, or call functions that use the template parameter. The definition
of the template is kept by the compiler until later needed for a specialization.
When the compiler creates a specialization, if a function call or an operator
has no meaning for a particular parameter, then a compile-time error occurs. In
short, the constraint for a template parameter is that it supports a particular
operation (i.e. it has a function with a suitable overload or it has a valid
operator). Template constraints are enforced at specialization rather than at
definition.
I use the phrase "lazy structural constraint" with much
liberty. The notion I am trying to get across is that the constraint is enforced
lazily because compiler diagnostics appear at specialization. They are
structural constraints because they require some kind of support from a type
parameter that is not necessarily dependent on a subtype relationship.
Templates support mix-ins. A class template can
inherit from a type parameter. This enables a number of programming patterns,
such as mix-ins and policy based programming. Generics do not support directly
inheriting from a type parameter.
Now, with that brief summary of templates out of the way,
let's turn to a brief summary of generics.
Generics are instantiated at run-time by the CLR.
Generics are also type safe.
Generics are cross-language.
Generics do not allow user-defined specialization.
Generics do not allow non-type parameters.
Generics use subtype constraints.
Generics are instantiated at run-time by the CLR.
Unlike templates, a generic defined in source code is emitted into MSIL as a
generic. The compiler does not specialize generics. A generic type thus has one
assembly to which it belongs. A programmer who wants to create an instantiation
of a generic must identify which assembly the generic comes from (either via
importing metadata or using the current assembly). When the runtime specializes
a generic, it creates one specialization for all ref classes. Each value type
will have its own specialization.
Generics are also type safe. Generics were designed
to be verifiable (meaning that the MSIL could be proved type safe). Like
templates, generics are only unverifiable if they use unverifiable features.
Generics are cross-language. By far the biggest
advantage to templates is that producers of cross-language frameworks need to
use generics instead of templates. While generics are not Common Language
Specification (CLS) compliant now, it is expected that they will be at some
point in the future.
Generics do not allow user-defined specialization.
As generics were designed as a runtime service, the designers of generics felt
that specialization was tied too much to the semantics of a particular language.
Thus, when writing a generic, it is only possible to write it once. This is like only being allowed to write a primary template.
Generics do not allow non-type parameters. The
primary design goal for generics was creating parameterized collections. Most
collections do not require non-type parameters, and thus the designers of
generics did not include this feature.
Generics use subtype constraints. This is the big
one. It is the mechanism by which generics are implemented on the CLR. First,
look at the following code:
generic<typename T>
ref class R {
void f(T t) {
t->g();
}
};
How do we know that the call to g in the function f is valid? With
templates, we'd check at specialization. With generics, it's up to the runtime
to determine that this is valid. In order to verify that a generic class is
valid, the runtime needs more information. Thus, we must fix the above
definition as follows:
generic<typename T>
where T : IG
ref class R {
void f(T t) {
t->g();
}
};
With generics, overload resolution is done at the point of
definition. Thus, the call to g is done by
looking for the name g in the constraints for
T. As there is only one constraint,
g must be a member of
IG. The g
function is called through the interface rather than directly on the variable.
Consider the following example:
interface class IMethod {
void f();
};
ref struct R : IMethod {
virtual void g() = IMethod::f {
System::Console::WriteLine("R::g");
}
void f() {
System::Console::WriteLine("R::f");
}
};
generic<typename X>
where X : IMethod
void G(X x) {
x->f();
}
template<typename X>
void T(X x) {
x->f();
}
int main() {
R^ r = gcnew R;
G(r);
T(r);
}
With generics, the call to f
is done through the interface IMethod. With
templates, the call to f is done directly on
the class R. Thus, the output of this program
is:
R::g
R::f
Explicit overrides (used for explicit interface
implementation in other languages) are not the only way this difference could be
surfaced. Function overloading will change too. Overloads within a generic only
consider the constraints as possible arguments, whereas with templates the
compiler waits until specialization so it only needs to do overload resolution
with the real type.
An unfortunate consequence of subtype constraints with
interfaces is that not all useful functions in a class can be contractually
guaranteed through an interface. An interface only demands virtual functions. So
non-virtual functions, static functions, static conversion functions, static
operators, and constructors cannot be used on a generic type parameter. As the
CLS requires operators and conversions to be static member functions, generics
cannot make use of operator overloading on generic type parameters. This means
that a type used in a sorted collection needs to implement a specific interface
rather than simply provide the less-than operator. That is a significant
drawback if you're used to templates, and this is why the Whidbey version of the
frameworks is updating all the built-in types to implement
IComparable<T>.
Now with all of that background, here is a comparison of
templates and generics:
Generics
Templates
Constraint mechanism
Subtype constraints
Lazy structural constraints
Allows explicit specialization
No
Yes
Allows partial specialization
No
Yes
Type identity of specialization
Globally unique
Unique to each assembly
Cross language facility
Yes
No
Allowed parameters
Ref class, value class only
All types and non-type
Name lookup and binding
At definition, to constraints
At specialization, to type
Certainly, there will be evangelists for either option. The
best option for C++ is to support both mechanisms. Having both templates and
generics satisfies anyone who believes one is better than the other. Of course,
any pragmatic programmer will realize that having both gives the programmer the
ability to use the right feature to solve the problem at hand. Both generics and
templates have shortcomings, but using both features together actually yields an
even more expressive language.
A very compelling scenario is using templates to create
highly efficient data structures, but exposing that type at the assembly
boundary through a generic interface. This is similar to a factory pattern that
uses private types that inherit from public interfaces. Using this pattern, a
specialized C++ collection class can take advantage of frameworks APIs that use
the interface and allows other languages to make use of the type through the
interface.
This technique is exactly how the STL.NET collection
library will be implemented. The collections will be C++ templates that employ
the STL design of iterators and separate algorithms. The collections will
implement generic interfaces such as IList<T>.
I think that the potential for combining templates and generics is great, and
we're just starting to scratch the surface. Much of uses for combining templates
and generics were driven by Anson Tsao, Martyn Lovell, and Eric Niebler while
they were investigating STL.NET.
Hopefully, this was a lucid (after all I am not sleeping)
explanation of the fundamental differences between generics and templates. As
both features are significant, I very well could write a dozen more pages. As I
did with handles, I will later spend time writing about the design rationale
behind both generics and templates.
Behind the Design: Handles
17/11/2003 external link
In this writing, I plan to discuss the history and
rationale of handles. This is perhaps the most noticeable addition to C++. I
have heard many questions about handles. Why does C++ need handles? Why are
they named handles? Why did you use the hat to declare them? And much more.
The design team spent quite a bit of time getting handles right.
First, it is most useful to know what handles are. I
will provide a short summary. C++ has the notion of declarators, which are
ways to build up types by adding symbols. The two declarators in standard
C++ are pointers (denoted with the asterisk) and references (denoted with
the ampersand). In addition to the previous declarators, C++ adds handles
(denoted with the caret). The caret symbol (^)
is often referred to as hat, just as the asterisk is referred to as star.
A handle refers to an instance of an object that is
garbage collected (note isn’t exactly true, we’ll clear that up later). How
does an object become garbage collected? There are two ways this is done:
(1) boxing can copy a value type and put it on the GC heap, or (2) C++
introduces the gcnew operator that creates
a new instance of any type on the GC heap. For example:
Button^ b = gcnew Button();
The new and
gcnew operator are similar. Both allocate
memory and invoke the constructor of the object. Whereas
new allocates memory from a native heap
and returns a pointer, gcnew will allocate
memory from the GC heap and return a handle. A boxed value type is easy to
recognize, as the type is simply a handle to value type. For example:
int m = 42; // integer on the stack
int* n = new int(42); // integer on a native heap
int^ o = gcnew int(42); // boxed integer on the GC heap
There are other ways to create boxed value types, but I
will leave that for another time when I discuss the implementation of boxed
value types.
Why is it important to distinguish whether an instance
of an object is on the GC heap or a native heap? The GC algorithm
implemented by the CLR is a generational compacting garbage collector. This
means that the memory location of the object can change upon each garbage
collection. This does not happen on the native heap. A pointer refers to an
instance in memory that never moves. The garbage collector ensures that a
handle always points to the right instance.
To access a member of an instance referred to by a
handle, use the arrow (->) operator. For
example:
Object^ o = f();
o->ToString();
Now, at this point someone reading this might think
that handles are awfully similar to pointers. Such an observation may lead
to questions as to why introducing handles to the language was even
necessary. To understand this, it does help to look back at the Managed
Extensions syntax that shipped with Visual C++ 2002. Much of the language
redesign has used the experience of implementing that language and user
feedback to evolve the C++ language. Let’s look at some of the problems with
what Managed Extensions tried to do.
Before we do that, here is a quick summary. In Managed
Extensions, there were three kinds of pointers. The first was a native
pointer (also known as a __nogc pointer)
which is a traditional meaning of pointer. It points to data in memory that
will not move. Another kind of pointer was a whole object pointer (known as
a __gc pointer). These pointed to
instances of __gc classes. The third kind
of pointer was an interior pointer (also known as a
__gc pointer), and these could point
anywhere and in particular inside objects on the GC heap. Managed Extensions
also had a feature known as defaulting rules, which allowed the compiler to
choose the most logical meaning for a pointer. Consider this example:
// System::String is a __gc class
System::String * s; // String __gc * (whole object pointer)
System::String __nogc * q; // ERROR – ill-formed type
System::String __gc * r; // String __gc * (whole object pointer)
// int is a __value class (Int32 is mostly the same as int)
int * i; // int __nogc *
int __nogc * j; // int __nogc *
int __gc * k; // int __gc * (interior pointer)
System::Int32 * l; // int __gc * (interior pointer)
System::Int32 __nogc m; // int __nogc *
System::Int32 __gc * n; // int __gc * (interior pointer)
// std::string is a __nogc class
std::string * v; // string __nogc *
std::string __nogc * v; // string __nogc *
std::string __gc * v; // ERROR – ill-formed type
The defaulting rules were introduced to make it easier
to write code. As seen above, a pointer to a __gc
class can only be a __gc pointer, and
pointer to a __nogc class can only be a
__nogc pointer. The only place where the
defaulting rules introduced difficulty was with value classes. In every
regard, int and System::Int32 are the same
type except that Int32 defaults to having __gc
qualification and int defaults to
__nogc qualification. Because
__gc qualification can be added but not
taken away (much like const and volatile), trying to pass a
__gc pointer to a function expecting a
__nogc pointer resulted in a compile-time
error. It was with this that we first saw users struggling. There are two
ways to resolve this compile-time error: (1) pin the
__gc pointer and convert it to a
__nogc pointer, or (2) change the function
to accept a __gc pointer. It was the
latter option that many people chose, and they did so by placing
__gc everywhere in the code until the
program compiled. In particular, when dealing with a sequence of pointers
(such as __gc pointer to a
__nogc pointer), it became clear that most
people did not understand how a pointer acquired
__gc qualification in the first place.
One advantage that the defaulting rules allowed was
function templates could be agnostic to __gc
qualification. For the most part, this is very useful; however, recall
though that the garbage collector can move memory pointed at by a
__gc pointer. After each garbage
collection, the value in a __gc pointer
could be different. Code that comparing less-than or greater-than of two
__gc pointers could return different
results before and after garbage collection. Such behavior is subtle, and
can easily lead to fragile code.
Also, for the code reviewer, the defaulting rules
required knowledge of what kind of type was being pointed to. If the type
was a __gc class or a
__value class, the code reviewer would
like to look for unwanted pointer tricks like conversion to
int and back.
Of course, the most significant drawback of the Managed
Extensions pointer qualification was the inability to overload operators on
__gc classes. The Base Class Library
defines a number of useful overloaded operators and C++ users clearly wanted
to use this functionality with the natural operator syntax. Pointers,
however, already have operators defined on them (such as equality,
less-than, dereference, and arrow). While it is conceivable that overloading
some operators on __gc pointers could be
done, it was impossible to do so cleanly.
With all that out of the way, the design team felt very
strongly that a simpler design was needed to lower the intellectual burden
of using the CLR GC heap. Handles solved the problem very nicely. They are
closest to the whole object pointer from Managed Extensions. Because handles
were freed from the compatibility of pointers, they were designed to afford
the programmer all the advantages of pointers while providing first-class
support for CLR features. In fact, handles have opened new possibilities in
the language. This is a sign of good language design.
First, handles have the ability to overload operators.
For instance, it is possible to write the following:
X^ operator+(X^ xl, X^ xr);
X^ x1;
X^ x2;
...
X^ x3 = x1 + x2; // calls operator+(X^, X^)
Making operators in the new language features work well
with the CLR has mostly been relaxing rules and making operators more
flexible. The operator overloading design in C++ was already quite solid. At
a later time, I will talk about how operators have changed.
Another useful outcome of handles is that C++ can take
advantage of conversion functions in the Base Class Library. If a class
referred to be a handle contains a user-defined conversion, the compiler
will now be able to find it.
During the design of handles, the design team has
worked towards offering the conveniences of pointers without the pitfalls.
For example, using a pointer as a Boolean expression is a useful way to
guard a member access. For example:
Y^ y = g();
if (y) y->Execute();
This is actually a very tricky thing to get right. In
C++, bool is an integral type that can convert to an
int via a standard conversion. These
standard conversions happen all the time. Clearly, we wanted to avoid
allowing every handle converting to integers. It would make it difficult to
diagnose improper arguments when many overloads to a function exist. Also
relying on a conversion function to exist in an ultimate base class does not
work, as System::Object is only a base
class for all ref class and value class types (it is not a base for native
class types). The design team solved this problem by introducing a
conversion function to a special Boolean type as a special member function.
C++ has a number of special member functions already. This is yet another
subject that I will write about later.
Handles do not have the same meaning as pointers. They
do not have built-in less-than, greater-than, increment, or decrement
operators. It is not possible to reinterpret_cast
a handle to an int and then back to a
handle. In every regard, handles are type safe. One of the design goals for
the new language was to make writing verifiable code easier. That is, code
should be verifiable the first time the code was written. A program that
makes use of pointers is nearly always unverifiable. The list of rules for
writing verifiable code is short, and among the rules is to use handles
instead of pointers.
One nice part of this new design is that defaulting
rules are not necessary. In C++, a pointer always refers to memory that will
not move. In large part, the defaulting rules are no longer necessary
because of gcnew. In the past,
new behaved differently on
int and Int32.
Now, new and
gcnew behave exactly the same way for both
int and Int32. In fact,
int and Int32
are exactly the same in C++. Another useful outcome of
gcnew is that the MFC debug macros do not
conflict with it, which will make it easier to use CLR features in existing
MFC programs.
Perhaps one of the most exciting prospects of handles
and the gcnew operator is that it was possible for the design team to lift
the restriction that native classes could not be garbage collected. There is
a significant amount of machinery to make this work, preserve existing C++
semantics, and implement a robust solution. This is part of the feature set
known as the "unified type system" of which I will have to spend much time
writing about. In short, the design team is making this work:
std::vector<int>^ vec = gcnew std::vector<int>;
This particular feature (creating handles to native
class types) unfortunately will not be part of the Whidbey feature set. As
with most software engineering projects, we had to make cut-off decisions so
we could deliver a solid compiler earlier.
If you’re interested in learning the manner in which
the CLR implements handles, look for discussion of "object references" in
the CLI standard and CLR documentation.
Stan Lippman deserves the credit for looking at a new
declarator. When he first started working on revising the language, he was
working on the notion of a rebindable reference. He first used the % symbol
as the declarator. Jeff Peil, who had also come to the conclusion that a new
declarator was needed, pointed out that % was not the best choice due to C++
digraphs. When the sequence of characters, %>,
is seen by the C++ lexer, it is replaced with a closing curly brace,
}. If handles were to be used as template
arguments (which they definitely are designed to be widely used in that
regard), the digraph behavior of the C++ Standard was undesirable. Of the
remaining unused symbols, the caret was the best. Nostalgic memories of
Pascal pointers are shared amongst all of us on the design team.
J
A curious result of the choice to use the caret is
another Standard C++ feature, alternative tokens. Wherever ^ is used, it is
perfectly suitable to also use the keyword, xor.
For example, the following is legal:
Button xor b = gcnew Button;
As a note to Visual C++ users, both digraphs and
alternative tokens are available only when compiling with the /Za switch.
The /Za switch conflicts with the /clr switch. As standards
conforming behavior on the CLR is still a goal for Visual C++, we do have a
strategy for finishing standards conformance features and making them the
default. At some point I can write about that too.
As for the unification feature, Herb Sutter is the one
to credit for driving that work. Although much of the details were figured
out by all five of us on the design team, he sold this to partners,
managers, and most importantly C++ developers. I think he helped push the
design team to figure out all the possibilities handles enabled.
I am leaving out discussion of getting handles from an
lvalue (such as address-of with & returns a pointer from an lvalue). I will
discuss that after more discussion of the unified type system and
deterministic finalization.
Mark Hall is the one to credit for the
gcnew operator. After doing most of the
design work for Managed Extensions, he has been the most qualified to
recognize ways to avoid the same issues.
Lastly, why are they named "handles"? At the beginning
of the language design, they were called either managed pointers or whole
object pointers. At times they were also called GC pointers and tracking
pointers. The problem with this is that they are not pointers. Any adjective
applied to pointers misses the point that they are not a modification to the
semantics of pointers, but instead they are an entirely different
abstraction. We noticed that discussion tended to confuse whether the
context was referring to native pointers or this new declarator. (During the
course of conversation, writing, or dialog, there is a tendency to drop
adjectives as more context is built up). The challenge was left to figure
out a better term. Brad Van Ee mentioned "handle" in a hallway conversation.
That has been the term to stick.
Throughout this and past writings, I have been
promising to write about a number of other topics. If anyone is more
interested to hear about one subject before another, give me feedback either
via comments or via email. Hopefully, this has been interesting and I am
happy to answer questions as they come up.
Table-pounding Evangelism of Visual C++
13/11/2003 external link
Today has been a long day. The language design team is
getting ready to release a draft of the language specification the the ECMA
technical group. The TG5 meetings for ECMA start in just under a month. It’s
very exciting, and the countdown is leading to a lot of last minute work. Look
forward to seeing the early draft of the language specification on the Visual
C++ dev center soon.
Anyways, someone forwarded a link around to an
InfoWorld article talking about Visual C++. His takeaways from my
presentation resonated with a number of other people I met at PDC. There is so
much to look forward to from Whidbey, I’m just glad that the Visual C++ message
is memorable enough weeks after PDC. We really do love smart developers, and
we’re working hard to support them.
For tomorrow, I am working on a history and rationale for
the design of handles in the language. A handle is a new declarator we are
adding, the caret (^), which implies garbage collection. It is the reason the
slogan on the bright yellow shirt passed out at the PDC is:
Visual
Can you handle ^ C++?
Security Improvements to the Whidbey Compiler
11/11/2003 external link
I’ve been away from an Internet connection over the last
few days. After a conference in the Netherlands, I visited my sister in Germany.
She’s stationed at Spangdahlem Air Base, which happened to be where the Air
Force started using Xbox Live. The German countryside is amazing. I spent most of
my time on the air base, but had time for a trip to Aachen.
Anyways, I was invited to speak about some Visual C++
security features at the Netherlands Unix Users Group’s autumn conference. When
I’m not working on language design, one of the things I like doing is looking at
security problems and doing something about them. When I started in Visual C++,
Louis Lafreniere and Phil Lucido had created a feature known as “security
checks” (commonly referenced as the “/GS switch”). This debuted in the Visual
C++ 2002 release. I wrote
an article that described how the feature worked in that release.
At the end of 2001, I had a number of discussions with
Louis and Phil about how we could improve security checks. As buffer overruns
were found, we saw that it was often possible to circumvent the security checks
architecture. There were a few cases where the VC 2002 implementation would have
prevented arbitrary code from running, but it was clearly possible to do better
in general. The discussion between Louis, Phil, and I led to a number of ideas,
some of which were introduced to Visual C++ 2003. The main thing that VC 2003
did was sort the local variables so that buffers were allocated in memory
addresses higher than other local variables. This prevents local variables from
being overwritten by a buffer overrun, thus avoiding attacks like pointer
subterfuge and v-table hijacking.
Our discussions also showed that the security checks
architecture was unable to prevent attacks that exploited exception handling
(such as Code Red). This is because a security check that determines whether a
cookie changed happens at the end of a function call in the function epilog.
Exceptions allow a program to choose control flow that avoids returning from a
function. What makes exception handling exploitable is that exception
information is placed on the stack (this is done for historical reasons and
performance). I’ll spend more time talking about exception handling later (as it
will be useful for understanding /EHs and /EHa). If a buffer overrun is able to
overwrite exception handling information on the stack, the EH info can be
somewhere earlier in the call stack. As many system libraries make use of
exception handling, nearly every program will have some exception records in the
call stack.
One thing that makes exceptions unique on Windows is that
the operating system provides the infrastructure to make exceptions work. For us
to make code resilient to attacks against exception handling we needed support
from the operating system. Bryan Tuttle, a build engineer in Windows, suggested
creating tables of exception thunks that Windows could use to validate the EH
record. This suggestion was developed into the feature known as “Safe
Exceptions”. Many people were involved to make this work, including Richard
Shupak, Dan Spalding, Louis Lafreniere, Phil Lucido, and Bryan Tuttle. This
feature debuted with the improvements to /GS in VC 2003. The operating system
infrastructure for safe exceptions was introduced in Windows Server 2003.
The VC 2003 release had a short development cycle, so not
all of our ideas to improve /GS were implemented. The Whidbey product cycle gave
us the opportunity to do more. The biggest improvement to /GS is the protection
for vulnerable function parameters. To understand this, it’s helpful to see what
the stack layout looks like before this change. From high memory to low memory,
this is what shows up in a function activation record:
Function arguments
Return address
Frame pointer
Cookie
EH record
Local buffers
Local variables
Callee save registers
If a buffer overrun occurs, it is possible to overwrite the
EH record, the cookie, the frame pointer, the return address, the function
arguments, and function activation records earlier in the call stack. The EH
record is protected by safe exceptions, the cookie can only be exploited by
using a value that matches the value in the __security_cookie variable, the
frame pointer is only useful after the function returns, and the return address
is only exploitable at the function return after the security check has already
taken place. Thus, during the execution of the function these parts of the
function activation record are not exploitable; however, the function arguments
are used by the code in the function. VC 2003 did nothing to protect the
function arguments.
The Whidbey compiler will do something to address this by
identifying vulnerable arguments and copying those arguments to memory addresses
lower than the local buffers. This is done in the function epilog. The code of
the function then makes use of the copy of the function argument rather than the
original argument. We often refer to this as parameter shadowing. This yields
the following stack layout:
Function arguments
Return address
Frame pointer
Cookie
EH record
Local buffers
Local variables and copies of vulnerable parameters
Callee save registers
Why only copy some parameters? Making copies of parameters
has a performance impact. Just as only vulnerable functions have cookies and
code injected for the security check, only vulnerable parameters in vulnerable
functions will be copied. What makes an argument vulnerable? Basically, it is an
inductive set: pointers and structures that contain vulnerable parameters. The
actual implementation of this feature does a more in depth analysis that
includes other factors to identify vulnerable parameters. Of course, there are
parameters that are vulnerable but cannot be moved, such as non-POD C++ objects.
Ultimately, the choice of copying vulnerable parameters is heuristic with a goal
of balancing performance with real security mitigation benefits.
This improvement makes it more difficult to use out
parameters and pass by reference variables to circumvent the security checks
architecture. For example, in VC 2002 an out parameter that was changed by a
buffer overrun to point to the __security_cookie variable would make it possible
for an attacker to get a predictable cookie value thus preventing the security
check in the function epilog from triggering. This then opens the possibility
for easier arbitrary code exploits such as stack smashing. The Whidbey compiler
will not make use of the original out parameter, so that approach to
circumventing the security checks architecture will not work.
The recently announced service pack to Windows XP and
Windows Server 2003 will be built with a compiler that includes these Whidbey
improvements. This will help improve Windows’s resilience in the event of a
buffer overrun, and will hopefully mitigate the harm a buffer overrun can incur.
It is our hope that a service pack to the VC 2003 compiler will include these
updates, and we are currently investigating how that may be possible.
Another change we are making in Whidbey is that /GS will be
the default behavior in the compiler. This follows from the trustworthy
computing pillars that software should be secure by default. All Microsoft
software is building with /GS. By making this the default, turning off security
checks requires an explicit action that can be found by grep of build logs. That
makes audits of code bases easier. The Visual C++ team has spent an enormous
amount of effort to make /GS useful for retail software, and the fact that
Windows, Office, SQL Server, Visual Studio, among other products build with this
proves that it is an effective feature.
I have left out a number of details. There are number of
other improvements to /GS that we are doing. I’ve been saying this for a year
now, but I do hope to have time to write a revision to the white paper I wrote
in February 2002. In the meantime, know that /GS is improving with each release
of Visual C++.
There is no language lower than C++
7/11/2003 external link
At the PDC, I was part of a panel that answered questions
on the future of programming languages. Naturally, I represented C++. I also
have a passion for functional programming languages, so I'm certainly a fan of
Erik Meijer who was the moderator. (We had a rather late night at the country
bar near Universal Studios the night before the panel if anyone at the panel was
wondering. J)
Perhaps the most talked about message from the panel was my
summary that "there is no language lower than C++." (I'll have some evidence to
back this up later in this writing.) Some people have interpreted my unbridled
enthusiasm for claiming that C++ developers are the smartest developers as a
measure to rescue C++ from C#. Oddly, some people compared C++ to Visual Basic.
It is quite amusing actually. I'm sure many people would find it interesting to
have a behind the scenes look at how C++, C#, and Visual Basic compare each
other.
First, the C++ language design team has made a point of not
being in competition with C#. In many ways, the introduction of a language in
the middle has helped Visual C++. Prior to the introduction of C#, there was a
number of advanced Visual Basic users that needed to go to C++ to get there job
done. Obviously, that's a tough leap. The low end of C++ isn't really a lot of
fun if you're not really into programming. So, with the introduction of C#, the
Visual C++ team has had the opportunity to focus more on the needs to advanced
developers and those who know and understand C++. One example of this happening
is the effort to make templates better in Visual C++ 2003 and to bring standards
conformance levels to a more respectable level.
At the PDC, I was asked whether C# thinks they're in the
middle. The answer is absolutely! A few years ago, the usability team at
Microsoft developed personas for programmers in each of the languages. The
Visual Basic developer was the "opportunistic programmer", the C# developer was
the "pragmatic programmer", and the C++ developer was kind of guy who knew the
whole system. Each of the personas were given names (yeah, I know I'm not
telling you what they are) so that all teams could identify with the programmer
for each language. What makes C# and C++ programmers different? Well, for one
C++ developers are really multilingual developers. They use the programming
language best suited for a task. A C++ programmer also tends to understand the
whole system, and doesn't mind reading MSIL or assembly code. C++ developers
also think in much broader contexts – they think about the architecture of the
whole program, rather than just single pieces. C++ developers also tend to
appreciate lower level components that can be combined to create simpler
components. (It's really very difficult to create components with more utility
from higher level components that have less utility). C++ programmers tend to
think about performance more often, and understand the dependencies in their
programs. C++ programmers try to maximize tools available to get things done
more efficiently (be it, sed, awk, profilers, lint tools, perl scripts, etc.)
C++ programmers will also build tools to get the job done when a tool is not
available.
There is much to be said about what a C++ programmer can
do. Obviously, not every developer would fit the mold of persona we developed.
We were trying to make sure we built a product that was best suited for a more
advanced developer and make that person productive. Every language has a goal of
being productive, but different kinds of programming tasks have different
demands and thus different ways of achieving productivity.
And of course one of the ways a C++ programmer is
productive is that he is not blocked from going to a lower level programming
style when necessary. Constantly, the C++ language design team has made an
effort to expose everything the CLR enables. Three examples come to mind:
A class should be allowed to be abstract and sealed.
The CLS disallows global functions, so a library writer targeting many
languages needs to create a utility class to contain static functions. A
utility class is not meant to be a data structure, and thus it is sealed.
Since all the functions in a utility class are static, it is abstract to
prevent instantiation. The notion of abstract and sealed are orthogonal to
each other, and while other languages disallow their combination (and in
some cases have even introduced alternative ways of expressing the same
concept), C++ allows it simply because the CLR allows it.
The ".override" directive in MSIL is used to
explicitly override a virtual function in a base class. Other languages
expose this feature only for explicitly implementing an interface method.
C++ provides syntax for getting to this CLR feature directly, and can be
used in contexts other than explicitly implementing an interface.
Properties and events really are a collection of
functions. This means that different accessibility levels for get and set
functions is possible, and it is also possible to bind delegates to the get
or set functions.
Of course, it should also be noted that C++ has the ability
to express things at a much higher level, and in some cases expresses concepts
in a much more consistent, correct, and succinct manner. An example of this is
destruction semantics in C++ that pretty much makes the Dispose Pattern
unnecessary. I'd even say that this makes C++ a less fragile programming
environment for large and long lived applications. I'll certainly spend time
talking about this in more depth later.
As always, I view C++ as an enabling language. It provides
a significant amount of power, and number of language tools to get the job done.
The C++ language design team strives to make answers to "can I do this"
questions yes whenever possible.
News of my death has been greatly exagerated
6/11/2003 external link
Good morning!
I really meant to write earlier. I've been busy preparing
for a trip to the Netherlands, where I am right now. I'll talk about that
tomorrow.
Anyways, I promised I'd talk about what I heard from C++
developers at PDC. After so many years of customer concern that Microsoft didn't
care about Microsoft, it was great to see how excited people were to know that
C++ has a vibrant future. Of course there was the introduction of the new
language design, but the message that I think was best received was that
rewriting applications to take advantage of WinFX is unnecessary and almost
always the wrong approach. Many people didn't know that existing C++ code can
run on top of the CLR without any modification.
That's right, you heard me. The CLR is rich enough to
express all of the C++ semantics. In Visual C++ 7.0, we introduced the /clr
switch. As a home experiment, just try it – take any program, compile it with
/clr, and chances are really high that it will just work. That is where the
acronym IJW (it just works) came from. I was surprised at how many people didn't
know about this. We're officially calling this feature (at least for now) C++
interop.
Of course, the great thing is that in Visual C++ "Whidbey",
this feature is improving. How can something that is named "It Just Works" be
improved? Well, for starters we can improve on some of the performance issues.
Undoubtedly, anyone who ran the experiment above would have taken notice of a
noticeable slowdown. In Whidbey, we've done several things to address this. Two
of them are the __clrcall calling convention, which allows a program to avoid
the top performance culprit "double thunking", and the /clr:pure switch which
also avoids double thunking, but enables some CLR features that did not work
with C++. Double thunking is an interesting topic, and I'll write about it
later.
Another way that "Whidbey" improves the IJW feature is that
we're doing a lot of the ground work in the libraries to take existing programs
and let them use the Frameworks. The best example is we're enabling MFC programs
to use WinForms. Naturally, much of the work will allow MFC to make use of other
Frameworks APIs too. That means all the MFC programs out there can start using
WinFX without ripping and replacing their current code.
Another thing I heard from PDC is that running on
down-level platforms that do not have WinFX is crucial. This is similar to
versioning the Win32 API. It was possible for programs to test whether the
operating system had a particular API, and then change behavior based on the
results. This was done with delay loading a DLL and using GetProcAddress. In the
WinFX world, it changes. We're currently working on creating the best practice
for doing this. It is sufficient to say that this will be possible.
Many customers asked how Microsoft applications are going
to take advantage of WinFX. Naturally, quite a few developers like to see how
Microsoft does something first. Just as many customers are looking into WinFX,
so are all the applications around Microsoft. Existing C++ applications (that's
pretty much everything Microsoft currently ships) are using Visual C++ to take
advantage of WinFX. A few places where other languages make sense, other
languages are being used. Another feature that Visual C++ "Whidbey" introduces
is MSIL linking, which allows anyone to take a .netmodule generated by any other
language, and link it into a C++ program as if the .netmodule were just another
object file.
I also got asked, "What is it like to work with Herb
Sutter?" Of course, Herb is great person to work with, and certainly brings
another point of view to discussions. I barely knew I was working with
celebrity. J It was too bad the
Southern California fires prevented Herb from showing up at PDC. At the same
time, Herb was at the C++ Standard WG21 meeting. Quite a few things are going on
there, but we also took a chance to let everyone else on WG21 know what was
going on with ECMA and the new language features that bind C++ to the CLI.
Another celebrity I was asked about is Stan Lippman. People
definitely missed him at the PDC. I completely forgot to mention Stan in my last
posting. Stan definitely has provided a number of contributions to the new
language design. As I get to discussing individual language elements in more
depth, I'll certainly show where Stan deserves much of the credit. Right now
Stan, one of the projects Stan is working on is a translation tool from the
managed extensions syntax to the new C++ syntax. In fact, Stan has the best
expertise in this area. Sooner or later, he too will have a blog. I'll mention
when that happens.
In the end, the one message I heard from customers was
"thank you". There are a lot of C++ developers out there who are very excited
about what Visual C++ "Whidbey" will deliver. I heard comments to the effect of
"Visual C++ is back" and "C++ is on its way to being the top language again."
We're very excited to see customers excited about Visual C++. C++ is definitely
the most enabling language I have ever worked with, and we can't wait to see
what C++ developers are going to do with WinFX.
Design guidelines for C++
30/10/2003 external link
I'm here at PDC, and it hass been great to meet so many C++
developers. Of course, the best part of this week is that Visual C++ has finally
announced the new language design for C++. As I mentioned last time, I will
spend a while covering rationale and design behind this language design. Before
going through the design process and some of the history in the language, I'll
cover highlights of what we learned from managed extensions.
Keeping compatibility with existing C++ was really
great. This is what we have colloquially called "It Just Works" or IJW.
Giving C++ developers seamless interop between unmanaged and managed code
was the killer feature. We needed to preserve this advantage and look at
making it even better.
People hate underscore-underscore keywords. Even
though this was done to work well with the C++ Standard (and developers
understood that), no one really liked it. Going forward we needed to look at
different ways to have first-class keywords but maintain compatibility with
existing C++ code.
Overloading the meaning of the same syntax makes the
language hard to use. Nowhere is this more obvious than with pointers. An
asterisk in the managed extensions syntax meant one of several things: (1) a
native pointer in which memory did not move, (2) an interior pointer in
which memory could move and it often pointed inside an object, or (3) a
whole object pointer in which memory could move. Looking at single line of
code and determining which kind of pointer was in use was sometimes
impossible. We learned that different concepts need to be expressed with
different syntax.
I could go on forever on lessons learned. A frequent
message that we heard was developers like C++ and wanted to use it, but managed
extensions were just too hard to use. More concerning is that some of the design
decisions in managed extensions actually prevented us from innovating in the
language. For example, the usage of pointers made it impossible to make use of
operator overloading CLR types, as user defined operators do not apply to
pointers. Based on significant feedback, we decided to redesign the CLR features
in C++.
Several things have greatly benefited the new language
design. First, we actively sought feedback from C++ community leaders. Compiler
vendors, library developers, and others were involved very early on. They were
given the opportunity to provide feedback, and we listened and acted. Second, we
started by setting goals for the language design. As more people were involved,
it was easier to get randomized with different approaches to solving problems.
By having a list of goals available, we were able to measure each of these
solutions.
Many people from Microsoft were involved in creating a
solid language design. From the compiler side, Mark Hall and Jonathan Caves have
brought significant experience in writing the Visual C++ compiler and doing much
of the work that made the Managed Extensions language possible. Jeff Peil
brought a wealth of knowledge about the C++ standard, the internal working of
the CLR, and a keen understanding of how C++ is used in production. Chris Lucas
led the language design team for a while to set the goals and direction for the
language design. Herb Sutter facilitated the community involvement and
standardization process, and recently started leading the design team to guide
it through some difficult decisions. Anson Tsao and Martyn Lovell have been
involved from the libraries aspect – both building libraries with the new
language and ensuring common C++ practices were enabled by the language design.
Finally, I have been involved working on the language semantics, implementation
schedule, and language specification document. Of course there a dozens more
people who are implementing this language and actively involved in the design.
As I cover more specific areas, I will mention who was involved.
Knowing that, Chris Lucas is the one to thank for setting
the language design team in the right direction. Extending a language is much
more difficult than creating a new language from scratch. He started by reading
Bjarne Stroustrup's book, "The Design and Evolution of C++." Many of the design
goals of C++ still resonated today, and they were adapted to the goals we
finalized upon. Here is a summary of those goals:
General Rule
Using CLR features is an option, not a requirement for
C++ development.
Design Rules
Solve problems developers have today.
The CLR features are a first class part of the
language, we need to make the experience seamless.
Enable good designs to be written. Even though C++ has
significant flexibility, we need to enable good developers to write robust,
correct, and maintainable code.
C++ is, and should always be, a multi-paradigm
language. We need to support existing paradigms (object oriented,
procedural, and generic) in everything we do. We should also make sure new
features are orthogonal so that the combination of features yields an even
more powerful language.
Consider the interaction with other .NET languages.
These aren't always just paradigm issues. We need to make sure that
frameworks programming is easy with C++.
We should have aspirations for standardization and
growth in the CLR support, but prioritize making C++ useful and productive
today.
Technical Rules
Care needs to be taken to preserve C++ semantics versus exposing
CLR semantics. Using both semantics at the same time is ideal.
Syntax should have a distinct and consistent meaning.
It's more important to enable C++ and CLR features, than it is to
disable them for CLS compliance. It should be obvious when a developer is
writing CLS compliant interfaces, and tools should assist the developer. If a
feature could be CLS compliant, make it CLS compliant.
We should not avoid a feature due to verifiability, but we should
ensure that it is possible to write productive, useful verifiable code in C++.
It should be easy for a developer to recognize when code is verifiable or not.
If a feature could be verifiable, make it verifiable.
Support Rules
Leave no room for a lower level language on the CLR
than C++.
Don't sacrifice the scalability of the C++ build
process.
Support the creation of CLS compliant interfaces and
verifiable components.
Support assembly, module, and compiland organization
of code.
The language enables the library which enables the
application.
There is a lot behind each of these rules. I'll spare
everyone from an in depth discussion of each one (someday it will probably end
up in a book). In the end, we had a very simple mantra that the language design
team lived by: Bring C++ to .NET. Bring .NET to C++. In that short phrase
lies a lot of power. C++ has so much to offer, as does .NET. It was our goal to
bring the best of both worlds together. In many ways, we believe we've succeeded
at tackling this challenge. As we continue to gain experience and feedback, we
are confident that C++ will continue to provide the power and flexibility that
make C++ developers the best in the world.
Next time, I'll share some
of the things I've heard at PDC as well as provide some background on the type
of developer the Visual C++ group is building a product for.
Introduction
27/10/2003 external link
Hey everyone! Before I get around to introducing myself,
let me take a moment to explain why I'm joining the ranks of other Microsoft
bloggers. For starters, I have found the information on other blogs extremely useful.
I've enjoyed contributing to the Visual C++
newsgroups and back when I taught sections in college I spent a lot of time
discussing technical stuff with my students. Now that I've spent a considerable
amount of time within Visual C++, there is so much I can share – both on Visual
C++ itself, C++ programming, and things in general at Microsoft.
With that said, I am the Program Manager for the Visual C++
Compiler front-end. I've been working in Visual C++ since June 2001. Over that
time I have worked on the compiler backend team and now the front-end team. The
best part of working in Visual C++ is that there are so many great people here.
In some ways, Visual C++ is at the crossroads of Microsoft – every team at
Microsoft makes use of the C++ compiler, so not only does Visual C++ have
traditional customers (those who buy Visual Studio) but every team at Microsoft
is also our customer.
To provide some context for Visual C++, I'd like to explain
the structure of the Visual C++ product group. These are the main teams:
Compiler front-end: This is the team I
currently lead. It is responsible for the C++ language. Sometimes, you may
see the phrase "programming models team." This refers to the collaboration
between the compiler front-end and the libraries team.
Compiler backend: This team does all the
optimizations and code-generation. Right now, they are responsible for
generating code for x86, IA64, AMD64, and MSIL.
Tools: Most notably, this includes the linker;
however, they do much more. The tools team keeps the product stable by
maintaining the build and code check-in process.
Libraries: The libraries team has created some
of the most notable programming styles from Microsoft including MFC and ATL.
They also maintain and improve the CRT. The libraries team is part of the
programming models team because they set the programming trends for many
Microsoft C++ programmers.
IDE: As expected, the IDE team works on the
Development Environment. Most of the IDE work is done by a team shared
amongst all of Visual Studio – the VS Core team. The C++ IDE team is
responsible for C++ specific work including the C++ debugger expression
evaluator, C++ IntelliSense, designers and wizards, and the C++ project
system.
Box: No, they do not design the box.
J Actually, they coordinate the
beta program, the schedule, the external team dependencies, and a whole
bunch of other tasks. Without the box team, we'd never get anything done.
Having experience on both parts of the compiler team, I
have gotten to know quite a few people. For a while, I also owned the CRT, so I
have some insight into the libraries space. And as mentioned earlier, the Visual
C++ team spends a lot time talking with other teams at Microsoft. As much as I
can, I will try to provide a mix of interesting content. For the next few weeks,
I will spend most of my time providing insight into the new C++ language design.
In my next posting, I will talk about the history of all the CLR features in C++
and the design goals for C++.


