Skip to main content
GameDev.net gamedev.net
🔒 Locked

Why does C++ require this? (rantish)

Started by Zahlman Oct 30, 2006 at 12:55 PM 15 replies 3.2k views
Original Post
Zahlman
Zahlman
This FB thread reminded me of one of my favourite (?) annoyances with C++. I present exhibit A:

#include <iostream>
using namespace std;

struct Foo {
	/*virtual*/ void wibble() { cerr << "foo::wibble" << endl; } // LINE A
};

struct Bar : Foo {
	void wibble(); // LINE B
};

void Bar::wibble() { cerr << "bar::wibble" << endl; } // LINE C

int main() {
	Foo f;
	Bar b;
	b.wibble();
	void (Foo::*x)() = &Foo::wibble;
	void (Bar::*y)() = &Bar::wibble; // LINE D
	(f.*(x))();
	//(f.*(y))(); // LINE E
	(b.*(x))(); // LINE F
	(b.*(y))();
}
Here I've created a function implemented in base and derived classes, as well as function pointers to the two versions. Obviously line E won't compile and is commented out (because a Foo isn't a Bar); it's just there so we see all the permutations of invoking a function pointer with an object. So, the beginner's complaint boils down to, effectively, "I didn't have line B and that caused line C to produce compile errors, although it seemed to work in this other case..." (where the other case is not reproducible, at least by me). And indeed, if we comment out line B, errors are reported on line D. So the question is, why is this? It seems inconsistent with the treatment of data members: we don't need to re-declare those (and in fact, doing so would duplicate the data within the derived object layout). But it gets worse when we consider what happens if we comment out both lines B and C: the program *still compiles*. In particular, line D still works. Think about that: Even though we can't define 'Bar::wibble' because "it doesn't exist", we can nonetheless make a pointer-to-member-function to it. And invoke it, too. Worse still, while the 'virtual' keyword is supposed to help us with doing things the "real OO way" (by enabling dynamic dispatch, and thus being key to "defining interfaces"), uncommenting it on line A doesn't even help here. The only visible effect it has is on line F (and then only when we have lines B and C both in, such that there is a different function available to to the dynamic dispatch).
So. The question is, how is the necessity of line B justified? It seems inconsistent with the treatment of data members - without redeclaration (there's no real separation of declaration and definition of class data members), we can 'use' the data members in all ways, whereas with the function members, we can use them in all ways *except to define them*. Suddenly, we need to declare in order to add a definition, even though we couldn't possibly *remove* the function from the derived class. We can't define Bar in such a way that calling wibble() on a Bar object is *illegal*, as long as it inherits from Foo (and to inherit from Foo, you do need to see the whole class declaration of Foo in that translation unit, so that the Bar data layout can be deduced). But even though that call *must* be legal, and even though the compiler is apparently able to deduce that fact (as proven by the ability to create and invoke PMFs to the call), we are forced to *re-assert* that fact before we can change what the call *does*. What's going on? Am I missing some subtlety? Is there "something dangerous" you could do otherwise, or some complication to the compiler logic that would otherwise be required? I sure can't think of anything, and I like to think I know this stuff pretty well...
Bregma
Bregma
I suspect that the compiler needs to know at comile time that Bar has its own definition for wibble(). It already knows Bar has a wibble() (since it inherited one from Foo), that why it can take its address.

If you could define the function without a declaration in the derived class, the definition might exist in a .cpp file somewhere, and the compiler would never know. This would have an impact when the compiler is trying to calculate the internal offsets in cases like multiple inheritance.

Stephen M. Webb
Professional Free Software Developer
Sharlin
Sharlin
Well, definitions may (and often do) reside in a different translation unit where the compiler has no way of reaching them — after all, the two-phase compilation paradigm is why we *need* declarations in the first place. There's no way the compiler could create correct vtables and emit correct code at a call site if it doesn't know whether a given base class function is overridden in a derived class. The same applies, of course, to static member variables (except obviously they can only be statically overridden).
NotAYakk
NotAYakk
Let's start with the meat. What is the difference between the classes Bar1 and Bar2?

#include <iostream>struct Foo {	/*virtual*/ void wibble() {		std::cerr << "foo::wibble" << std::endl;	} // LINE A};struct Bar1 : Foo {	void wibble(); // LINE B};struct Bar2: Foo {	//void wibble(); // LINE B2};


There is one simple difference: Bar1::wibble() is linkable, but Bar2::wibble() is not.

When you do:
void (Bar1::*y1)() = &Bar1::wibble; // LINE Dvoid (Bar2::*y2)() = &Bar2::wibble; // LINE D

and you look for Bar1::wibble and Bar2::wibble, the C++ compiler searches for what you mean by Bar1::wibble and Bar2::wibble. It looks in the class, the parent classes, and does a heck of alot of work.

But by the end of the line, it knows what you meant by Bar#::wibble. It has a unique, linkable, name.

In the case of virtual inheritance, things are somewhat different.

The writers of C++ considered linking to be hard. So they made linking not much harder than C linking. Each compolation unit imports and exports fixed named symbols.

The linker stage needs to know little to nothing about the details of the C++ language. It simply takes the pre-compiled object code, does the external symbol lookups, substitutes the correct values for the external variables in each object block, and goes on with it's day.

What would happen if you didn't have to declair member functions?

Well, you couldn't resolve the what the single, unique name of the &Bar2::wibble function in a given compolation unit. You'd have to say "Bar2::wibble if it exists, and Foo::wibble if Bar2::wibble does not exist", in effect.

This makes the linking stage much more complicated. And this is a relatively simple situation -- with a large heirarchy, it can require searching the entire heirarchy with a particular order of precidence to find the function in question.

In effect, the linker would have to know a heck of alot about the details of the C++ language, because you couldn't resolve what the heck the programmer wanted during the compilation stage.

...

With virtual functions, you don't have a pointer to a function -- instead you have directions on where to find the function in the virtual function table. Completely different, and wonkier, case.
Enigma
Enigma
Quote:
Original post by Zahlman
So the question is, why is this? It seems inconsistent with the treatment of data members: we don't need to re-declare those (and in fact, doing so would duplicate the data within the derived object layout).

Does it?

Redeclaring a data member provides a new data member with the same name and hides the base class data member. The base class data member is still accessible if explicitly requested.

Redeclaring a member function provides a new member function with the same name and hides the base class member function. The base class member function is still accessible if explicitly requested.

Seems pretty consistent to me.

Σnigma
Zahlman
Zahlman
Quote:
Original post by Bregma
If you could define the function without a declaration in the derived class, the definition might exist in a .cpp file somewhere [i.e. in a different translation unit], and the compiler would never know.


That would seem to be the crux of it, at least as concerns linking.

Or as NotAYakk put it:

Quote:

You'd have to say "Bar2::wibble if it exists, and Foo::wibble if Bar2::wibble does not exist", in effect.

This makes the linking stage much more complicated. And this is a relatively simple situation -- with a large heirarchy, it can require searching the entire heirarchy with a particular order of precidence to find the function in question.


Although I'm not sure it'd be that much more difficult, and certainly not impossible. (On the other hand, "complicated" here might be interpreted as referring to time-complexity, and frankly, linking can be frighteningly slow as it is. Further, all kinds of link-time optimizations are known about but not, to the best of my knowledge, commonly done - yet. Imagine how long things *could* take!)

I can imagine an argument about PLS violation when you define Bar::wibble in its own translation unit and then neglect to link it in, though.

But the real problem seems to be the linking model.

Quote:
Back to Bregma for a bit...
This would have an impact when the compiler is trying to calculate the internal offsets in cases like multiple inheritance.


I don't really see how. The vtable (if it exists) is per-class rather than per-object, and the offsets to vtable pointers (and to the "real data members" from the start of the structure) only depend on the bases, not the virtual functions defined therein.

Quote:
Original post by Enigma
Quote:
Original post by Zahlman
So the question is, why is this? It seems inconsistent with the treatment of data members: we don't need to re-declare those (and in fact, doing so would duplicate the data within the derived object layout).

Does it?

Redeclaring a data member provides a new data member with the same name and hides the base class data member. The base class data member is still accessible if explicitly requested.

Redeclaring a member function provides a new member function with the same name and hides the base class member function. The base class member function is still accessible if explicitly requested.

Seems pretty consistent to me.


This is interesting, too. It seems like the apparent inconsistency (where another rational person sees perfect consistency) is the result of separate "declaration" and "definition" being meaningful concepts for functions, but not for data. And also due to the fact that you usually think of functions as "overriding" rather than "hiding" - although this common way of thinking causes problems when you change the function signature in the derived class. (Because the base form is always available - even though it doesn't "take up space" - it seems like "hiding" more accurately describes the usual, and thus the general, case.)




One reason why I ask is that I intended to "fix" this in the language I'm working on (starting to work on quite slowly) - but in initial versions, I wasn't planning to worry about any kind of "incremental compilation" support whatsoever. Am I going to be in a world of hurt later? How much harder is it to do a "real" import/module system ala Python or Java or etc. ? If "not much, or perhaps it's even easier", why didn't C do it? (Too computationally expensive? But perhaps not as computationally expensive as the changes I propose? :s)
NotAYakk
NotAYakk
Quote:
This is interesting, too. It seems like the apparent inconsistency (where another rational person sees perfect consistency) is the result of separate "declaration" and "definition" being meaningful concepts for functions, but not for data.


Data has "declaration" and "definition".

struct bar {  int const foo; // declaration of foo  bar(): foo(7) {} // definition of foo};


As it happens, functions in C++ are implicitly "const pointers to functions" -- they cannot be changed once initialized, and must be initialized exactly once.

There just happens to be different syntactic sugar for initializing const variables and initializiong methods.
Nitage
Nitage
Quote:
Think about that: Even though we can't define 'Bar::wibble' because "it doesn't exist", we can nonetheless make a pointer-to-member-function to it. And invoke it, too.


If Line B isn't commented out, then Bar::wibble does exist - it's an overidden function.

If Line B is commented out, then Bar::wibble still exists - but it's the same thing as Foo::wibble.

If the requirement to declare an override were removed then you'd have to know the contents of every single translation unit in your program to know the behaviour of a method call on any derived class. So even if it were a simple chnage to the compiler, maintaining any such code would be a nightmare.

Think about the analagous situation with a const static integer variable:

struct foo{    const static int wibble = 2;};struct bar : foo{    const static int wibble;//Error without this line};const int bar::wibble = 3;


It all seems consistent to me.
Zahlman
Zahlman
Quote:
Original post by NotAYakk
Data has "declaration" and "definition".

struct bar {  int const foo; // declaration of foo  bar(): foo(7) {} // definition of foo};


As it happens, functions in C++ are implicitly "const pointers to functions" -- they cannot be changed once initialized, and must be initialized exactly once.

There just happens to be different syntactic sugar for initializing const variables and initializiong methods.


You seem to equate definition with initialization. Even for a const identifier, I'm not sure I'm terribly happy with that :\ But I do think I see your point.

Quote:
Original post by Nitage
If the requirement to declare an override were removed then you'd have to know the contents of every single translation unit in your program to know the behaviour of a method call on any derived class. So even if it were a simple chnage to the compiler, maintaining any such code would be a nightmare.


Okay, so basically this is the PLS argument. :) Point well taken.

Quote:

If Line B is commented out, then Bar::wibble still exists - but it's the same thing as Foo::wibble.


Mm, that does seem to be the core of it. It seems that "the same thing as" here is intended to mean identity, whereas my rant is predicated on expecting it to mean equality. That is, the compiler isn't copying Foo::wibble as a default implementation for Bar::wibble, but rather causing references to Bar::wibble to alias Foo::wibble. I can live with that.




Now, suppose we discuss the idea in a language-agnostic way. Can we get around such a limitation in a language with similar syntax (without sacrificing static typing or completely destroying the compilation model)? Would we want to restrict where things are defined relative to their declaration, in order to simplify the task? I assume these ideas are largely responsible for how Java syntax works - do we need to be so draconian? And are there any conceivable reasons for lifting (ways to lift?) the need for this kind of declaration, but not for declarations in general? [smile]
MaulingMonkey
MaulingMonkey
As a practical measure, one of the game gem programming books (I think it was) advocates the use of #defines to implement largely repeated groups of virtual function declerations. Something similar along the lines of "interface" (class-like) and "implements" (inheritence-like) keywords could at least mirror this in a less langage unfriendly manner, something along the lines of:

interface iwibble {    void foo( int    );    void bar( char   );    void baz( double );};class base : implements iwibble {};void base::foo( int    ) { ... }void base::bar( char   ) { ... }void base::baz( double ) { ... }class derived : public base , implements iwibble {};void derived::foo( int    ) { ... }void derived::bar( char   ) { ... }void derived::baz( double ) { ... }
Zahlman
Zahlman
Mm. I can't begin to guess the impact on incremental compilation, but I had been planning to handle interfaces by inference. It would look like (this also illustrates a few other things I had in mind):

class base: # This actually generates the interface base and the class _base  to foo(_int a): # X    # For non-void functions, the 'X' comment would be replaced by an expression    # yielding the default return value, where the type would be inferred.    # There would probably need to be some special syntax to decide whether the    # returned thing should be of a static (_-preceded) or dynamic type    # (something like "managed reference to something that is LSP-substitutable    # for the expression's result").  to bar(_char b):  to baz(_double d):  # Unlike Python, I don't intend to require a "pass" keyword for empty suites.  # Also, the type names for primitives will probably end up being different :)  # (I intend to have "primitives"; they will have some object-like behaviour,  # but not necessarily behave polymorphically: i.e. there is separate _int  # and int, where the 'int' interface subsumes 'numeric' etc.class derived can base: # declare that it's LSP-substitutable.  # I will probably offer synonyms for 'can' so that this can be made to read  # as nicely as possible.  data:    local _base does base # this is as close as it gets to inheritance:    # the methods of the interface, if not otherwise handled, are all    # automatically delegated to the base member.    # 'local' is as opposed to 'gc' or 'refcount' or 'weak'; data members    # all get automatically wrapped up in handles, where the keyword indicates    # what kind of handle to use, in combination with other factors.    # For example, a 'local base', being polymorphic, would be indirected    # through a simple smart pointer with copy-by-virtual-clone semantics;    # a 'local _base' could be held in a "by value handle" that doesn't actually    # indirect through a pointer at all, but pretends to for uniformity of    # generated code. (Did I mention that the first draft of the compiler will    # just cross-compile to C++?)    # Speaking of which, "local base does base" would be OK too; that's a kind    # of "virtual inheritance" that's not quite what C++ means by the term ;)  to foo(_int):    # This would be an override; bar and baz would be automatically delegated    # still. I still haven't thought very much about name collisions  to wibble(): self # something like "return PolymorphicHandle<Derived>(this);"  # The 'bar' interface is inferred to consist of all capabilities of _bar  # that aren't already accounted for by 'can' specifications. I.e., just  # wibble(), here.  # You could suppress generation of _bar (with an effect similar to making  # the class "abstract") or bar (similar to making it "final"); I think I  # ought to disallow suppression of both at once. This would be done, in  # current plans, with "cannot bar"/"cannot _bar" at the top. :)# I haven't really thought about separating functions out from the class # definition, either. I might well just stick with the Java model. Sure seems# easier to implement :)


I guess this is quite off the original topic now, though X_X
NotAYakk
NotAYakk
Bah. If you are going to write a new language, at least solve double-virtual-dispatch (or n-ary-virtual-dispatch).

Functions-in-objects is just fancy syntax for "pass the parameter as the first arguement". It also lets you have that first arguement change the function pointer at will.

But there is no reason why you can't have the "virtual parameter" be the 2nd one:
void Foo( int x, virtual Bar b )

Then Foo( x, b ) is simply b.Foo( x, b ).

With multiple dispatch, you get more than one virtual arguement:
void Foo( virtual Bar b, virtual Baz c )

and once that is in the language, you can now do things that Java and C++ find hard, easily.

...

There is more than one reason to make a language.

You can do it to get rid of annoying features of other languages.

You can do it to make something easy that is hard in another language.

You can do it to make something hard that is easy in another language. (usually things you consider "bad" that should be avoided)
Zahlman
Zahlman
I did plan on it, actually. :) The plan is to compile a list of all the overloads of the functions, and look for any ambiguities (by taking each pair of overloads with the same argument count, and unioning the capabilities of the parameters pairwise, and seeing if those capability-sets could be satisfied; if so, there exist sets of arguments that could be dispatched to more than one function, so a special-case overload has to be added by the programmer); then, for the purpose of dispatching to that function, construct a graph of the overloads topologically sorted from most to least specific; at runtime, the graph (lattice?) is traversed (using dynamic casts of some sort under the hood, to see if the current option is OK) until a valid overload is found, and finally it is invoked.

Something like that, anyway. I'll have a better idea once I start implementing, which is a long way down the road. First I want to get code ready that just generates interface and implementation classes from a single source, adds virtual clone() member functions, etc., along with getting my various handle types working.

I also have planned special syntax for the equivalents of dynamic_cast downward - "assuming {obj} can {capability}: {suite, wherein those functions may be called upon obj} [else: {suite, wherein they can't}]" and static_cast upward - "forget {obj} can {capability}" (which should be capable of affecting the dynamic dispatch I think; otherwise it probably ends up being useless).
NotAYakk
NotAYakk
Some suggestions:

If you have:
class FooClass;

FooClass foo;

then:
foo.bar()
is the same as
FooClass::bar(foo);

Ie: get rid of implicit this pointers.

With your interface/implementation duality, this can be extended. What if your variables where simply syntax for declairing both storage(implementation) and accessor(interface)?

ie:
class FooClass {  int x;};


Then
FooClass foo;
foo.x; is the same as FooClass::x(foo); It also creates an implementation of FooClass::x(foo) that has local storage x and reads/write from it.

Now, why would you want this? Because it gives you power and flexibility.

Your member functors now get access to the class they are contained in, at call time, and don't have to store it. It removes the special case of "member functions have access to the this pointer, but nothing else does".

If you think of the problem as "functions" and "objects" that they act on, and not as "objects had have functions"... Then the problem of "what object we have" and "what functions to call" becomes interesting!

I do like your dynamic cast.

Are you planning on having all three categories of arguements to functions?
pure Input, Input/Output, and pure Output?

C/C++'s (and heck, most languages) "pure Output" parameters are crippled. All you have is the one return value, and it semantically implies multiple copies.

Planning on an operator MOVE?

If your dynamic_cast function can return two different TYPES of return value, possibly you should generalize it.

What if you didn't special case that, and allowed anyone to write a multi-type return function?

// get_mutator can return either a ClassA or a ClassB{ClassA OR ClassB} get_mutator();// a multi-type switch statement// "result" is the name of the switch statementbranch result (get_mutator()) {  // if the return value   result is ClassA: {  }   result is ClassB: {  }  default: {  }}


Then your dynamic_cast becomes "not a special case". :)

Yes, I am insane.
Zahlman
Zahlman
Quote:
Original post by NotAYakk
Ie: get rid of implicit this pointers.


Yes, that transformation will presumably have to be done at least internally; I hadn't considered allowing the user explicitly use one to mean the other, but I suppose it couldn't hurt. (I do want to avoid Python-style "explicit self" within member function implementations, though.)

Quote:
With your interface/implementation duality, this can be extended. What if your variables where simply syntax for declairing both storage(implementation) and accessor(interface)?
Then
FooClass foo;
foo.x; is the same as FooClass::x(foo); It also creates an implementation of FooClass::x(foo) that has local storage x and reads/write from it.

Now, why would you want this? Because it gives you power and flexibility.


Interesting. IIRC, Eiffel does something like this. I was thinking about it: when you describe a data member, it would by default be 'local' (not sharable by other instances of things, owned by the current object, and completely private, even to other instances of the same class). You could override the sharedness with keywords describing the memory management policy (e.g. 'weak', 'gc', 'refcount'), and the access by clauses describing who can access it (e.g. 'read requires _foo write requires _foo' == C++ private; 'read requires foo write requires foo' roughly equivalent to 'protected', except working with any LSP-substitutable class, not just those derived by inheritance).

Quote:
Are you planning on having all three categories of arguements to functions?
pure Input, Input/Output, and pure Output? C/C++'s (and heck, most languages) "pure Output" parameters are crippled. All you have is the one return value, and it semantically implies multiple copies.


I haven't really thought about "real pure output". The problem is that the output-parameter has to exist for the caller ahead of time, and get passed in, or else there's no way for the caller to capture it. I might try to figure out a way to make it easier to return tuples, though. But the plan is to pass everything by const reference by default, unless a keyword is used to imply non-const reference passing, or for certain "primitive" types (which are still objects, but not polymorphic ones), pass by const value. (I know passing by const value is silly in non-generated C++, but it would be needed for consistency. It might be possible to optimize the generated code after the fact so that it actually uses a non-const value pass and modifies the parameter instead of the user's explicit copy...)

I should explain about function returns: the idea is that an expression after the ':' on the first line provides an initialization of the "default return value" (from which the type is inferred). Within the function, the function name can be used, VB-style, to refer to current default return value, and at end of function, or at a bald 'return' statement, said value is returned. 'return ' gets translated into 'function_name: expr; return;', basically.

I think this one is one of the few really good ideas to come out of VB. How often have you seen code that accumulates changes to a "result" variable and returns it at the end? With my combined approach, you are forced to return something valid (because you will at worst return the default value that you had to specify in order to get a return type), you gain the convenience of an implicit 'result variable', and you don't lose the convenience of 'return expr'.

Of course, if there is nothing after the ':' introducing the function, then the return type is void, with all the usual implications :)

Quote:

Planning on an operator MOVE?


I don't think I'll need it (although the generated code might sometimes - or even often - return things within handles with transfer semantics, ala auto_ptr). I did plan on a swap operator, though:

a : thing() # assign default-constructed thing to a,             # which is inferred to be of type thing            # (with a keyword, you could make it be type _thing)b : thing()a <:> b # a and b are swapped, more efficiently than doing it yourself        # because the handles (which need to be polymorphic) can swap pointers        # instead of you cloning a temporary.


Quote:

If your dynamic_cast function can return two different TYPES of return value, possibly you should generalize it.

What if you didn't special case that, and allowed anyone to write a multi-type return function?


(etc.) I don't really want to get into complications with type unions. Type intersections will be enough pain for now, I think, and unions are less useful: the example you propose could be dealt with by returning something with the common functionality, and then using 'assuming' blocks for A and B functionality.

get_mutator() : common()  # implementationthing: get_mutator()assuming thing can classA:  # stuffotherwise:  assuming thing can classB:    # stuff  otherwise:    # default


Before you cry 'misfeature', let me remind you what you said about wanting to make certain things difficult. [smile] I *do* want to discourage external polymorphism of this sort (especially with multiple dispatch available - it should be extremely rarely useful, and never necessary). Hence the 'assuming' line being its own syntax, and 'thing can classA' *not* being a boolean expression that could be substituted in where you like.




You know what, I think I should start a new thread in Software Engineering, like the one ApochPiQ had for Epoch.. :)
NotAYakk
NotAYakk
Quote:
Original post by Zahlman
Quote:
Original post by NotAYakk
Ie: get rid of implicit this pointers.


Yes, that transformation will presumably have to be done at least internally; I hadn't considered allowing the user explicitly use one to mean the other, but I suppose it couldn't hurt. (I do want to avoid Python-style "explicit self" within member function implementations, though.)


And why not have a Python-style "explicit self"?

It reduces confusing for new folk. It adds a bit of typing, admittedly.

Admittedly, I'd rather have the ability to mutate a function definition, and add implicit parameters...

So you have a function:
void foo(int x) {  this->y = x;}


foo has a "dangling variable" called "this".

In most languages, it would look for it in the global scope, and if it couldn't find it, it would fail to compile. What if didn't look for it in the global scope, and simply noted the dangling variable named this?

Then you could have:

mutator member(object this_, incomplete_function f) {  return bind( f, this, 'this' );}


That would be stealing a page out of mathematical logic. A function would have to be compile-time completed before it could be called.

Similarly, the use of global variables would have to be done explicitly (avoiding misbinds).

Quote:
Quote:
With your interface/implementation duality, this can be extended. What if your variables where simply syntax for declairing both storage(implementation) and accessor(interface)?
Then
FooClass foo;
foo.x; is the same as FooClass::x(foo); It also creates an implementation of FooClass::x(foo) that has local storage x and reads/write from it.

Now, why would you want this? Because it gives you power and flexibility.


Interesting. IIRC, Eiffel does something like this. I was thinking about it: when you describe a data member, it would by default be 'local' (not sharable by other instances of things, owned by the current object, and completely private, even to other instances of the same class). You could override the sharedness with keywords describing the memory management policy (e.g. 'weak', 'gc', 'refcount'), and the access by clauses describing who can access it (e.g. 'read requires _foo write requires _foo' == C++ private; 'read requires foo write requires foo' roughly equivalent to 'protected', except working with any LSP-substitutable class, not just those derived by inheritance).


I'm somewhat against a proliferation of keywords. When possible, I think primitives should be used, and 'keywords' build up out of them.

If your languages has a compile-time grammer that generates code and exposes/hides things, why not let the programmer write it, and include a standard grammer?

Of course, that shouldn't nessicarially be in a first implementation.

Quote:
Quote:
Are you planning on having all three categories of arguements to functions?
pure Input, Input/Output, and pure Output? C/C++'s (and heck, most languages) "pure Output" parameters are crippled. All you have is the one return value, and it semantically implies multiple copies.


I haven't really thought about "real pure output". The problem is that the output-parameter has to exist for the caller ahead of time, and get passed in, or else there's no way for the caller to capture it.


{int x, int y, double d} = foo();

The function foo could be given access to x,y and d for construction only.

Quote:
I should explain about function returns: the idea is that an expression after the ':' on the first line provides an initialization of the "default return value" (from which the type is inferred). Within the function, the function name can be used, VB-style, to refer to current default return value, and at end of function, or at a bald 'return' statement, said value is returned. 'return ' gets translated into 'function_name: expr; return;', basically.


I rather dislike this plan. It makes it harder to have compile-time errors.

Nearly every time you eliminate a compile-time error, you introduce a possible run-time error.

If the person writing the function forgets to return the proper value, what would be a compile-time error in most sensible languages becomes a run-time "we returned default without meaning to".

I like the ability to do type introspection of expressions (ie, typeof), but the requirement that you make an expression that returns the type you want seems obtuse.

And it seems pretty damn easy to accidentally say "this function returns 0 by default. Crap, I meant 0 in a 32 bit floating point number, not a 0 integer!"

Quote:
I think this one is one of the few really good ideas to come out of VB. How often have you seen code that accumulates changes to a "result" variable and returns it at the end?


And I'd support something like this.

{int x, int y, double d} foo() {  result<int> result_x(x);  result_x = 7;  result_x++;  return y(result_x+5), d(3.1415);}




Quote:
Quote:

Planning on an operator MOVE?


I don't think I'll need it (although the generated code might sometimes - or even often - return things within handles with transfer semantics, ala auto_ptr). I did plan on a swap operator, though:

a : thing() # assign default-constructed thing to a,             # which is inferred to be of type thing            # (with a keyword, you could make it be type _thing)b : thing()a <:> b # a and b are swapped, more efficiently than doing it yourself        # because the handles (which need to be polymorphic) can swap pointers        # instead of you cloning a temporary.


Sometimes moving an object uses significantly different semantics than assignment.

And sometimes the programmer has to do the work for you, because you (the compiler) cannot figure it out automatically.

Of course, this might be simplified if you remove the possibility to have an actual instance of an object, and restrict objects to reference existence only. Not certain.

Quote:
Quote:

If your dynamic_cast function can return two different TYPES of return value, possibly you should generalize it.

What if you didn't special case that, and allowed anyone to write a multi-type return function?


(etc.) I don't really want to get into complications with type unions.


Wasn't talking about a type union. I was talking about a function that can return either one of two different types.

The types need not ever exist in the same block of memory.

In ASM, imagine a function that returns either an INT in register R1, or a DOUBLE in register F1. When it returns, it actually returns to a different point of execution depending on which value it returns.

But, you know, done up pretty.

In effect, your dynamic cast code is a special case -- a function that can return more than 1 distinct type. Depending on what type your dynamic cast returns on, the function returns into different code -- which means your code is never executing with the wrong type in the wrong branch.

Why restrict this beautiful compile-time safety feature to your dynamic cast operator?

If you are going to include the language feature, why not include the primitives required to write the language feature in your language, and then write it in your standard library?

Quote:
Before you cry 'misfeature', let me remind you what you said about wanting to make certain things difficult. [smile] I *do* want to discourage external polymorphism of this sort (especially with multiple dispatch available - it should be extremely rarely useful, and never necessary). Hence the 'assuming' line being its own syntax, and 'thing can classA' *not* being a boolean expression that could be substituted in where you like.


Watch a useful usage:
{error OR special_case OR game_state} check_gamestate( game_state );branch evaluate (check_gamestate( game_state )) {  evaluate is game_state: {    // normal result  };  evaluate is special_case: {    // special case code  };  evaluate is error: {    // do error checking  };}


The interface for the check_gamestate indicates it could return an error, a special game state, or a game state. It requires the caller to pay attention, and unless it returns a game_state, the caller cannot examine the game_state it returned. So you can return semi-dangerous things (such as pointers), and arrange it so the caller cannot look at them unless they are valid.

The way to do this in C++ is to tell the caller in comments, or create pseudo-valid objects that indicate that they are invalid (like NULL pointers).

This pushes a compile-time checkable result into run-time checking.

Quote:
You know what, I think I should start a new thread in Software Engineering, like the one ApochPiQ had for Epoch.. :)


So, I was supposed to reply over there, or here? :)

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.