views:

226

answers:

3

I have hit upon a real brain scorcher in C++, it has never happened to me before.

The gist of the problem is that upon invocation of my (template) function the arguments I have defined defaults for have their values scrambled. It only happens if I call the function with the defaults.

My template function is declared like this:

template <typename T>
vector2<T> transform(vector2<T> const &vec, matrix4<T> const &m, T z = T(0), T w = T(1));

It is later, in the same header, defined like this:

template <typename T>
inline vector2<T> transform(vector2<T> const &vec, matrix4<T> const &m, T z, T w)
{
 vector4<T> res = m * vector4<T>(vec.x, vec.y, z, w);
 return vector2<T>(res.x, res.y);
}

Now when I call this with defaults (transform(vector2<double>(0, 1), view_transform)) I don't get the values I expect. Stepping into transform with VC++s debugger I see z and w having "funny" values (which in my experience means something isn't initialized properly).

Example funny values would be: 0.0078125000000000000 and 2.104431116947e-317#DEN

Now I've tried finding the answer on C++ FAQ Lite, googling it; even tried to calm myself with Schubert, but I can't for the life of me figure it out. I'm guessing it's really simple and I suspect it's some kind of template tomfoolery at work.

Is there a way to get the default values I expect and want, and why does it do this to me?

Edit 1:

If I the change call so it uses floats instead (transform(vector2<float>(0, 1), view_transform)) the problem goes away. It appears this only occurs if T = double.

Edit 2:

It only happens if I have two specializations for double and float. If I use a float specialization in one place the double specialization gets weird default values. If I change all the places the function is called so it uses double the problems "goes away". I still don't understand why though, it's like it's using faulty offsets or something when setting up z and w.

Edit 3:

Tales from the C++ Crypt:

#include <sgt/matrix4.hpp>

int main(int argc, char *argv[])
{
    sgt::matrix4<double> m0(
        2, 0, 0, 1,
        0, 2, 0, 1,
        0, 0, 1, 0,
        0, 0, 0, 1);

    m0 *= m0;

    sgt::vector2<double> blah0 = sgt::transform(sgt::vector2<double>(1, 0), m0);

    sgt::matrix4<float> m1(
        2, 0, 0, 1,
        0, 2, 0, 1,
        0, 0, 1, 0,
        0, 0, 0, 1);

    m1 *= m1;

    sgt::vector2<float> blah1 = sgt::transform(sgt::vector2<float>(1, 0), m1);

    printf("%f", blah0.x);
    printf("%f", blah1.x);
}

In matrix4.hpp:

// ...

template <typename T>
vector2<T> transform(vector2<T> const &vec, matrix4<T> const &m, T z = T(0), T w = T(1));

template <typename T>
inline vector2<T> transform(vector2<T> const &vec, matrix4<T> const &m, T z, T w)
{
    vector4<T> res = m * vector4<T>(vec.x, vec.y, z, w);
    return vector2<T>(res.x, res.y);
}

// ...

If I run that, the double-specialization has it's default arguments correct, but the float version gets both it's default arguments as zero (0.000000) which albeit better, it's still is not z = 0 and w = 1.

Edit 4:

Made a Connect issue.

+1  A: 

I do not know if this will work, but try using a static_cast instead of a C style cast for your default values.

*Edit: Apparently, the problem is the compiler.

Alerty
Yields the same results sadly.
Skurmedel
Technically that wasn't a C-style cast but a parameterized constructor call.
Drew Hall
@Drew Hall: It is not a constructor call.
Alerty
@Alerty: It's constructing a temporary T object (where T is double in this case) using a single-argument constructor. In the case of double, that's indistinguishable from a cast, but in some cases it's not.
Drew Hall
@Drew Hall: Sorry, I understood that you said the function itself was a constructor.
Alerty
+2  A: 

Is the code optimized? Maybe that's why the debugger is showing you the wrong values.

I tried this simpler code (in g++ 4.3.3) and it works as expected.

template <typename T>
T increment(T a, T b = T(1))
{
    return a + b;
}

int main()
{
    double a = 5.0;
    std::cout << increment(a) << ", ";
    std::cout << increment(a, 3.0) << "\n";
}
Matt Curtis
It shows the same behaviour in both release and debug (optimization off), but checking out if it made a difference I discovered a strange thing. It only happens if T is double, not if it is float?!
Skurmedel
+5  A: 

The following fails for my in Dev Studio:

#include "stdafx.h"
#include <vector>
#include <iostream>

template <typename T>
std::vector<std::vector<T> > transform(std::vector<std::vector<T> > const &vec,
                                       std::vector<std::vector<std::vector<std::vector<T> > > > const &m,
                                       T z = T(0), T w = T(1));


template <typename T>
std::vector<std::vector<T> > transform(std::vector<std::vector<T> > const &vec,
                                       std::vector<std::vector<std::vector<std::vector<T> > > > const &m,
                                       T z, T w)
{
    std::cout << "Z" << z << "\n";
    std::cout << "W" << w << "\n";

    return vec;
}

int _tmain(int argc, _TCHAR* argv[])
{
    std::vector<std::vector<int> >  xi;
    std::vector<std::vector<std::vector<std::vector<int> > > > mi;
    transform(xi,mi);

    std::vector<std::vector<float> >    xf;
    std::vector<std::vector<std::vector<std::vector<float> > > > mf;
    transform(xf,mf);

    std::vector<std::vector<double> >   xd;
    std::vector<std::vector<std::vector<std::vector<double> > > > md;
    transform(xd,md);
}

Output:

Z0
W1
Z0
W1.4013e-045
Z2.122e-314
W3.60689e-305

So I suppose it does not work as expected!!!

If you remove the pre-declaration and put the default arguments in the template function then it works as expected.

#include "stdafx.h"
#include <vector>
#include <iostream>

template <typename T>
std::vector<std::vector<T> > transform(std::vector<std::vector<T> > const &vec,
                                       std::vector<std::vector<std::vector<std::vector<T> > > > const &m
                                       T z = T(0), T w = T(1))
{
    std::cout << "Z" << z << "\n";
    std::cout << "W" << w << "\n";

    return vec;
}

int _tmain(int argc, _TCHAR* argv[])
{
    std::vector<std::vector<int> >  xi;
    std::vector<std::vector<std::vector<std::vector<int> > > > mi;
    transform(xi,mi);

    std::vector<std::vector<float> >    xf;
    std::vector<std::vector<std::vector<std::vector<float> > > > mf;
    transform(xf,mf);

    std::vector<std::vector<double> >   xd;
    std::vector<std::vector<std::vector<std::vector<double> > > > md;
    transform(xd,md);
}

This works as expected.
This has something to do with the template pre-declaration not actually being a function pre-declaration and thus it does not actually have default parameters and as such you are getting random values in the parameter list.

OK. Not from my reading of the standard this should work as expected:

Using n2521
Section 14.7.1 Implicit instantiation
Paragraph 9

An implementation shall not implicitly instantiate a function template, a member template, a non-virtual member func- tion, a member class or a static data member of a class template that does not require instantiation. It is unspecified whether or not an implementation implicitly instantiates a virtual member function of a class template if the virtual member function would not otherwise be instantiated. The use of a template specialization in a default argument shall not cause the template to be implicitly instantiated except that a class template may be instantiated where its complete type is needed to determine the correctness of the default argument. The use of a default argument in a function call causes specializations in the default argument to be implicitly instantiated.

The bold part of the paragraph seems (to me) to indicate that each specialization created because of default arguments will be implicitly instantiated into the translation unit when used.

Paragraph 11:

If a function template f is called in a way that requires a default argument expression to be used, the dependent names are looked up, the semantics constraints are checked, and the instantiation of any template used in the default argument expression is done as if the default argument expression had been an expression used in a function template specialization with the same scope, the same template parameters and the same access as that of the function template f used at that point. This analysis is called default argument instantiation. The instantiated default argument is then used as the argument of f.

Indicates that even if the default arguments are template parameters they will be correctly instantiated.

Well I hope I interpreted that correctly. :-)

Martin York
Could you try to call it with two different specializations (see my latest edit), like using both double and float? It's starting to smell like VC++ is doing something funny here.
Skurmedel
Haha oh dear, never knew default values could be this evil.
Skurmedel
Has anyone tried this on g++/MinGW? If it's illegal C++ then surely the C++ standard would mandate a compiler error message...?
j_random_hacker
@Martin: Are you sure this was your final code? The first code snippet won't compile due to a missing comma in the first declaration.
j_random_hacker
Just compiled the 1st code snippet (plus comma) under g++ 4.1.2 on Linux and it runs perfectly. **I think it's a compiler bug.**
j_random_hacker
@ j_random_hacker: Formatting got that. I was manually removing tabs and stuff and removed the comma.
Martin York
@ j_random_hacker: Before we declare compiler bug we need to deccide what the correct behavior is. And this is one of the obscure corners of the language.
Martin York
@Martin York: True (although if UB is allowed for this then that would be monstrous even by C++ standards...) Where are you litb? :)
j_random_hacker
@j_random_hacker: @litb. It think its a compiler bug. Check the parts of the standard I added to the answer above and let me know.
Martin York
first sample tested on borland c/c++ compiler 5.6.4 (from C++Builder 6). No problems
Alsk
@Martin: The second snippet that you quote looks more relevant than the first to me. I think it's also relevant that a function, which can be declared several times (one of which may be also be a definition), "accumulates" default arguments with each declaration (8.3.6) and I assume the same is true of function template declarations although I can't find anywhere that it's stated explicitly (e.g. not in 14.5.5).
j_random_hacker
@Martin: As an aside, I suggest changing the top line of your post which is now a bit misleading.
j_random_hacker