1

In the below code example, I have a class with a templatized 1-argument constructor, called Acceptor1. I want to pass in objects of type Foo, Bar, or Baz to its constructor, chaining the constructor calls. (The reality is that this simplified example reflects something I am trying to do with type erasure: where I want to pass in an object to the eraser class.) What actually happens is that when I try to declare an Acceptor1 object and pass it e.g. a Foo object, I define a variable that points to a function that accepts a FUNCTION pointer (not an Foo object) and returns an Acceptor1. The type of the function pointer is Foo(*)(). The overall variable is declared of type Acceptor1 (Foo (*)()).

I can't work around this with any possible enable_if statement, as shown by the Accessor2 class example, where I didn't even define a 1-argument constructor.

Like I mentioned, the end-use of this is to be able to have a type eraser class that has a constructor looking like Acceptor1, plus the extra mechanics for type erasure. I've done this and it works fine as long as the type I'm erasing isn't constructed with a 0/1-argument constructor. Or, I can declare and assign the variables in two separate statements. However, I have a lot of them, and this doubles the size of the code, plus takes up a few more CPU cycles, I guess.

Does anyone have any ideas on how to get Acceptor1 test( Foo() ) to be treated as defining an Accessor1, and initializing it with a Foo object? It works fine with Accessor1 test( Baz(d, d2) ), a two-argument situation. It also works fine with literals, e.g. Acceptor1 test( Bar(1.0) ). Thanks!

Sean

Here's the code:

#include <iostream>
#include <typeinfo>

using namespace std;

struct Foo {
    Foo() {
        cout << "Foo" << endl;
    }
};

struct Bar {
    Bar(double a) {
        cout << "Bar" << endl;
    }
};

struct Baz {
    Baz(double a, double b) {
        cout << "Baz" << endl;
    }
};

struct Acceptor1 {
    template<typename T> //no possible enable_if could help this problem
    Acceptor1(T f) {
        cout << typeid(T).name() << endl;
    }
};

struct Acceptor2 {
};

int main()
{
    double d, d2;

    std::cout << std::endl;

    //0 arguments - captures a conversion constructor
    Acceptor1 one(Foo()); //F9Acceptor1PF3FoovEE=Acceptor1 (Foo (*)())
    cout << "one: " << typeid(one).name() << endl << endl;
    
    //0 arguments - the result we expect
    Acceptor1 two = Acceptor1(Foo()); //9Acceptor1=Acceptor1
    cout << "two: " << typeid(two).name() << endl << endl;

    //There's no possible way to enable_if it out - I deleted the whole constructor
    Acceptor2 three(Foo()); //F9Acceptor2PF3FoovEE=Acceptor2 (Foo (*)())
    cout << "three: " << typeid(three).name() << endl << endl;

    //1 arguments - captures a conversion constructor
    Acceptor1 four(Bar(d)); //F9Acceptor13BarE=Acceptor1 (Bar)
    cout << "four: " << typeid(four).name() << endl << endl;
    
    //1 arguments - the result we expect
    Acceptor1 five = Acceptor1(Bar(d)); //9Acceptor1=Acceptor1
    cout << "five: " << typeid(five).name() << endl << endl;

    //There's no possible way to enable_if it out - I deleted the whole constructor
    Acceptor2 six(Bar(d)); //F9Acceptor23BarE=Acceptor2 (Bar)
    cout << "six: " << typeid(six).name() << endl << endl;

    //1 arguments - literal
    Acceptor1 seven(Bar(5.0)); //9Acceptor1=Acceptor1
    cout << "seven: " << typeid(seven).name() << endl << endl;

    //2 arguments - the result we expect
    Acceptor1 eight(Baz(d, d2)); //9Acceptor1=Acceptor1
    cout << "eight: " << typeid(eight).name() << endl << endl;
    
    //2 arguments - the result we expect
    Acceptor1 nine = Acceptor1(Baz(d, d2)); //9Acceptor1=Acceptor1
    cout << "nine: " << typeid(nine).name() << endl << endl;

    using FooMaker = Foo(&)();
    using AcceptorFnToBazMaker = Acceptor1(*)(FooMaker); //PF9Acceptor1RF3FoovEE=Acceptor1 (*)(Foo (&)())
    cout << "AcceptorFnToBazMaker: " << typeid(AcceptorFnToBazMaker).name() << endl << endl;

    return 0;
}

EDIT: It was suggested that this is a duplicate of Default constructor with empty brackets - I agree that they both mention the "most vexing parse" as the source of the problem, but the questions are different. The answer there even includes an example with a function of 2 parameters. In my case, with constructors, only 0/1-argument constructors are treated specially.

Sean
  • 393
  • 2
  • 11

1 Answers1

0

Change Acceptor1 test( Foo() ) to Acceptor1 test((Foo())) or an even better way would be Acceptor1 test{Foo()} or perhaps (as you did) Acceptor1 test = Acceptor1(Foo());

The compiler can interpret the line as a valid function declaration and an object initialization. The standard (as far as I remember) says that in such a case, take it as a function declaration. Here's what's in cppreference about this :

In case of ambiguity between a variable declaration using the direct-initialization syntax (1) (with round parentheses) and a function declaration, the compiler always chooses function declaration. This disambiguation rule is sometimes counter-intuitive and has been called the most vexing parse.

And hence you get this.

To make the compiler interpret this as an object initilization, add a parathesis () around to make it an invalid function declaration. Viz-a-viz, all you have to do is to make sure it's not a valid function declaration syntax.

theWiseBro
  • 1,439
  • 12
  • 11
  • Thanks much! I don't suppose you know where in the standard that behavior is defined, or even a web reference. Will mark as answered (in no time!). – Sean Mar 18 '20 at 06:09
  • @Sean added reference from cppreference, which I believe should be a valid and sufficient source of truth. – theWiseBro Mar 18 '20 at 06:25
  • I'd add an upvote if I wasn't too new. Thanks. – Sean Mar 18 '20 at 18:47