Skip to main content
gamedev.net
🔒 Locked

[c++] function parameter stack order

Started by jkv Apr 5, 2009 at 11:48 AM 8 replies 1k views
Original Post
jkv
jkv
Question relating to calling conventions (parameter order on stack). Code example and embedded question:
template<size_t N>
struct IFunction {
    virtual ~IFunction() {}
    virtual double eval(const double* vars) = 0;
};

template<>
struct IFunction<3> {
    virtual ~IFunction() {}
    virtual double eval(const double* vars) = 0;
    
    // Should I do this 
    double eval(double x, double y, double z) {
	// Is this safe, and if not how easily it's going to break ?
	return this->eval(&x);
    }
    
    // Or this ?
    //double eval(double x, double y, double z) {
    //	double vars[3] = {x,y,z};
    //	return this->eval(vars);
    //}
};

struct MyFunctionImpl3 : public IFunction<3> {
     virtual double eval(const double* vars) {
        // compute something...
     }
}
Compiled on g++ 3.4.5 (mingw) and worked as I expected. Haven't tested on linux/g++ or msvc yet but I believe it should work even then. If I restrict this on x86 and "standard" calling conventions, I assume it should work. But then again, is this still a case of disaster in waiting or simply very bad practice ?
Rewdew
Rewdew
I suppose it's both something that'll bite you back some day and bad practice...
What's your goal exactly: "optimizing" the call by doing a single push instead of 3 or just avoid creating that double[3] locally?
Note that if you know it'll be 3 doubles, you might as well do it the other way around: have the method that takes a double const * pass vars[0], vars[1] and vars[2] to the one that takes 3 arguments.
And thiscall with msvc is right to left, if I remember properly (and this goes via ecx).
Zahlman
Zahlman
Passing the parameters as 'double* vars' at all is a bad idea, to be honest. If you intend that the function always takes an array of 3 doubles, then pass an array by reference (like double (&)vars[3] - the parentheses around the & are important, since otherwise you are trying to declare an array of references, which is illegal) or a struct (or better yet, use boost::array). If not, you need some kind of length information (using "null termination" for an array of float data is a quite bad idea for a variety of reasons).

Oh, I think I see what you want to do: you want to pass N doubles to eval() for a IFunction, in general.

Umm... just use boost::function or related tools. Please. Here be many, large dragons.
jkv
jkv
Quote:
What's your goal exactly: "optimizing" the call by doing a single push instead of 3 or just avoid creating that double[3] locally?

Yes basically a slight optimization for 1,2,3 variable IFunction convenience method. Initially I thought it wouldn't work at all because cdecl or thiscall pushes arguments right-to-left but then afterwards remembered push actually decreases stack...

Quote:
Note that if you know it'll be 3 doubles, you might as well do it the other way around:
have the method that takes a double const * pass vars[0], vars[1] and vars[2] to the one that takes 3 arguments.

Nice, I could declare that 3 argument method as virtual instead of the array one and then let the derived class implement that. Only drawback I see would be that each specialized IFunction would need matching specialized implementing class. Currently only one implementing class is enought (if being lazy). Thanks for the idea!
Rewdew
Rewdew
One implementing class is enough but you're not really sure how many double's you'll get passed... As for the (micro-)optimization, you'd better leave that to the compiler, it's usually quite good at it. The whole virtual table lookup will probably be more of a hit than pushing the arguments on the stack anyway.
ddyer
ddyer
One of the "features" of the C language is that the order of
evaluation of arguments is not guaranteed. For example, "test"
might return either 1 or 2. Most compilers on most machines
will do it one way or the other, but you'll be unpleasantly
surprised when "portable" code is ported.

int bar(int *x)
{ *x=1;
}
int mumble(int *x)
{ *x=2;
}
int foo(int x,int y)
{ return(x+y);
}
int test()
{
int a=0;
foo(bar(&a),mumble(&a));
return(a);
}
see http://www.andromeda.com/people/ddyer/topten.html for more.
---visit my game site http://www.boardspace.net - free online strategy games
Rewdew
Rewdew
That's not what he's doing...
SiCrane
SiCrane
Quote:
Original post by jkv
Haven't tested on linux/g++ or msvc yet but I believe it should work even then.

MSVC doesn't like it too much. If you output the arguments in eval() like so:
  virtual double eval(const double * vars) {    std::cout << vars[0]  << "\n" << vars[1]  << "\n" << vars[2] << "\n";    return 0.0;  }

And call base version with foo.eval(1.0, 2.0, 3.0); it spits out:
12.00214e-3072.13097e-314
during one run, which happens to be quite different from the inputs.
Zahlman
Zahlman
Quote:
Original post by SiCrane
And call base version with foo.eval(1.0, 2.0, 3.0); it spits out:
12.00214e-3072.13097e-314
during one run, which happens to be quite different from the inputs.


Heh. I'm guessing the wrong values are from somewhere else on the stack? That doesn't seem like what the results would look like from an endianness or alignment issue....
SiCrane
SiCrane
Probably. 1.0 and 2.0 have the binary representation of 0x3ff0000000000000 and 0x4000000000000000, but 2.00214e-307 and 2.13097e-314 have the representation of 0x0041ff04c7504110 and 0x0000000101152921.

Topic Locked

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

Sign in to reply to this topic.