Chris Beck Chris Beck - 23 days ago 6
C++ Question

type-erasure, delegates, and lambda functions, versus msvc

In the following code, I'm using a relatively simple type erasure technique. The class

interpreter_context_ptr
defines an "interface", and pointers to objects implementing the interface can be used to construct an
interpreter_context_ptr
. This allows polymorphism without using virtual dispatch.

This is extremely similar to an old article called Impossibly Fast Delegates. Also, check out the eraserface project by badair on github.

Also, if you don't recognize it at first, the
+[](...)
syntax here is the so-called "positive lambda" syntax, here's a good explanation.

Here's the MCVE:

#include <iostream>
#include <string>
#include <vector>

class interpreter_context_ptr {
void * object_;
void (*new_text_call_)(void *, const std::string &);
void (*error_text_call_)(void *, const std::string &);
void (*clear_input_call_)(void *);

public:
void new_text(const std::string & str) const {
this->new_text_call_(object_, str);
}

void error_text(const std::string & str) const {
this->error_text_call_(object_, str);
}

void clear_input() const { this->clear_input_call_(object_); }

template <typename T>
explicit interpreter_context_ptr(T * t)
: object_(static_cast<void *>(t))
, new_text_call_(+[](void * o, const std::string & str) {
static_cast<T *>(o)->new_text(str);
})
, error_text_call_(+[](void * o, const std::string & str) {
static_cast<T *>(o)->error_text(str);
})
, clear_input_call_(+[](void * o) { static_cast<T *>(o)->clear_input(); }) {
}
};

/***
* Tests
*/

struct A {
void new_text(const std::string & str) {
std::cout << "A: " << str << std::endl;
}

void error_text(const std::string & str) {
std::cout << "A! " << str << std::endl;
}
void clear_input() { std::cout << std::endl; }
};

struct B {
void new_text(const std::string & str) {
std::cout << "B: " << str << std::endl;
}

void error_text(const std::string & str) {
std::cout << "B! " << str << std::endl;
}
void clear_input() { std::cout << std::endl; }
};

int main() {
std::vector<interpreter_context_ptr> stack;

A a;
B b;

stack.emplace_back(&a);
stack.back().new_text("1");
stack.emplace_back(&b);
stack.back().new_text("2");
stack.emplace_back(&b);
stack.back().new_text("3");
stack.back().clear_input();
stack.pop_back();
stack.back().error_text("4");
stack.emplace_back(&a);
stack.back().error_text("5");
stack.pop_back();
stack.back().error_text("6");
stack.pop_back();
stack.back().new_text("7");

stack.back().clear_input();
stack.pop_back();
std::cout << "Stack size = " << stack.size() << std::endl;
}


The code under test is extremely simple, just a few lines, and works well in gcc and clang in a project I worked on in the last few months.

However I get some quite terse error messages from MSVC which I don't understand.

First, it complains about the positive lambdas, which I think it shouldn't.
(I am pasting these errors from rextester.com)

Error(s):

source_file.cpp(27): error C2061: syntax error: identifier 'T'
C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\INCLUDE\xmemory0(655): note: see reference to function template instantiation 'interpreter_context_ptr::interpreter_context_ptr<A>(T *)' being compiled
with
[
T=A
]
C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\INCLUDE\xmemory0(773): note: see reference to function template instantiation 'void std::allocator<_Ty>::construct<_Objty,A>(_Objty *,A &&)' being compiled
with
[
_Ty=interpreter_context_ptr,
_Objty=interpreter_context_ptr
]
C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\INCLUDE\xmemory0(773): note: see reference to function template instantiation 'void std::allocator<_Ty>::construct<_Objty,A>(_Objty *,A &&)' being compiled
with
[
_Ty=interpreter_context_ptr,
_Objty=interpreter_context_ptr
]
C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\INCLUDE\xmemory0(918): note: see reference to function template instantiation 'void std::allocator_traits<_Alloc>::construct<_Ty,A>(std::allocator<_Ty> &,_Objty *,A &&)' being compiled
with
[
_Alloc=std::allocator<interpreter_context_ptr>,
_Ty=interpreter_context_ptr,
_Objty=interpreter_context_ptr
]
C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\INCLUDE\xmemory0(917): note: see reference to function template instantiation 'void std::allocator_traits<_Alloc>::construct<_Ty,A>(std::allocator<_Ty> &,_Objty *,A &&)' being compiled
with
[
_Alloc=std::allocator<interpreter_context_ptr>,
_Ty=interpreter_context_ptr,
_Objty=interpreter_context_ptr
]
C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\INCLUDE\vector(929): note: see reference to function template instantiation 'void std::_Wrap_alloc<std::allocator<_Ty>>::construct<interpreter_context_ptr,A>(_Ty *,A &&)' being compiled
with
[
_Ty=interpreter_context_ptr
]
C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\INCLUDE\vector(928): note: see reference to function template instantiation 'void std::_Wrap_alloc<std::allocator<_Ty>>::construct<interpreter_context_ptr,A>(_Ty *,A &&)' being compiled
with
[
_Ty=interpreter_context_ptr
]
source_file.cpp(68): note: see reference to function template instantiation 'void std::vector<interpreter_context_ptr,std::allocator<_Ty>>::emplace_back<A*>(A *&&)' being compiled
with
[
_Ty=interpreter_context_ptr
]
source_file.cpp(68): note: see reference to function template instantiation 'void std::vector<interpreter_context_ptr,std::allocator<_Ty>>::emplace_back<A*>(A *&&)' being compiled
with
[
_Ty=interpreter_context_ptr
]
source_file.cpp(28): error C2593: 'operator +' is ambiguous
source_file.cpp(28): note: could be 'built-in C++ operator+(void (__cdecl *)(void *,const std::string &))'
source_file.cpp(28): note: or 'built-in C++ operator+(void (__stdcall *)(void *,const std::string &))'
source_file.cpp(28): note: or 'built-in C++ operator+(void (__fastcall *)(void *,const std::string &))'
source_file.cpp(28): note: or 'built-in C++ operator+(void (__vectorcall *)(void *,const std::string &))'
source_file.cpp(28): note: while trying to match the argument list '(interpreter_context_ptr::<lambda_3268dba1ab087602b708c8fa2c92932b>)'
source_file.cpp(30): error C2061: syntax error: identifier 'T'
source_file.cpp(31): error C2593: 'operator +' is ambiguous
source_file.cpp(31): note: could be 'built-in C++ operator+(void (__cdecl *)(void *,const std::string &))'
source_file.cpp(31): note: or 'built-in C++ operator+(void (__stdcall *)(void *,const std::string &))'
source_file.cpp(31): note: or 'built-in C++ operator+(void (__fastcall *)(void *,const std::string &))'
source_file.cpp(31): note: or 'built-in C++ operator+(void (__vectorcall *)(void *,const std::string &))'
source_file.cpp(31): note: while trying to match the argument list '(interpreter_context_ptr::<lambda_b251fc93653023678ada88e17e2a71b3>)'
source_file.cpp(32): error C2061: syntax error: identifier 'T'
source_file.cpp(32): error C2593: 'operator +' is ambiguous
source_file.cpp(32): note: could be 'built-in C++ operator+(void (__cdecl *)(void *))'
source_file.cpp(32): note: or 'built-in C++ operator+(void (__stdcall *)(void *))'
source_file.cpp(32): note: or 'built-in C++ operator+(void (__fastcall *)(void *))'
source_file.cpp(32): note: or 'built-in C++ operator+(void (__vectorcall *)(void *))'
source_file.cpp(32): note: while trying to match the argument list '(interpreter_context_ptr::<lambda_3ba87b1970191b4772ddfa67a05f70ea>)'
source_file.cpp(28): error C2088: '+': illegal for class
source_file.cpp(31): error C2088: '+': illegal for class
source_file.cpp(32): error C2088: '+': illegal for class


Okay, something's wrong with the "positive lambda" theory in microsoft land, wherein the lambda gets implicitly converted to a function pointer and the unary
operator +
is a no-op. That's fine, let's get rid of the positive lambdas.

template <typename T>
explicit interpreter_context_ptr(T * t)
: object_(static_cast<void *>(t))
, new_text_call_([](void * o, const std::string & str) {
static_cast<T *>(o)->new_text(str);
})
, error_text_call_([](void * o, const std::string & str) {
static_cast<T *>(o)->error_text(str);
})
, clear_input_call_([](void * o) { static_cast<T *>(o)->clear_input(); }) {
}


Turns out MSVC is still not happy -- the primary error is the cryptic C2061.


The compiler found an identifier where it wasn't expected. Make sure that identifier is declared before you use it.

An initializer may be enclosed by parentheses. To avoid this problem, enclose the declarator in parentheses or make it a typedef.

This error could also be caused when the compiler detects an expression as a class template argument; use typename to tell the compiler it is a type.


Here's the full log:

Error(s):

source_file.cpp(27): error C2061: syntax error: identifier 'T'
C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\INCLUDE\xmemory0(655): note: see reference to function template instantiation 'interpreter_context_ptr::interpreter_context_ptr<A>(T *)' being compiled
with
[
T=A
]
C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\INCLUDE\xmemory0(773): note: see reference to function template instantiation 'void std::allocator<_Ty>::construct<_Objty,A>(_Objty *,A &&)' being compiled
with
[
_Ty=interpreter_context_ptr,
_Objty=interpreter_context_ptr
]
C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\INCLUDE\xmemory0(773): note: see reference to function template instantiation 'void std::allocator<_Ty>::construct<_Objty,A>(_Objty *,A &&)' being compiled
with
[
_Ty=interpreter_context_ptr,
_Objty=interpreter_context_ptr
]
C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\INCLUDE\xmemory0(918): note: see reference to function template instantiation 'void std::allocator_traits<_Alloc>::construct<_Ty,A>(std::allocator<_Ty> &,_Objty *,A &&)' being compiled
with
[
_Alloc=std::allocator<interpreter_context_ptr>,
_Ty=interpreter_context_ptr,
_Objty=interpreter_context_ptr
]
C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\INCLUDE\xmemory0(917): note: see reference to function template instantiation 'void std::allocator_traits<_Alloc>::construct<_Ty,A>(std::allocator<_Ty> &,_Objty *,A &&)' being compiled
with
[
_Alloc=std::allocator<interpreter_context_ptr>,
_Ty=interpreter_context_ptr,
_Objty=interpreter_context_ptr
]
C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\INCLUDE\vector(929): note: see reference to function template instantiation 'void std::_Wrap_alloc<std::allocator<_Ty>>::construct<interpreter_context_ptr,A>(_Ty *,A &&)' being compiled
with
[
_Ty=interpreter_context_ptr
]
C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\INCLUDE\vector(928): note: see reference to function template instantiation 'void std::_Wrap_alloc<std::allocator<_Ty>>::construct<interpreter_context_ptr,A>(_Ty *,A &&)' being compiled
with
[
_Ty=interpreter_context_ptr
]
source_file.cpp(68): note: see reference to function template instantiation 'void std::vector<interpreter_context_ptr,std::allocator<_Ty>>::emplace_back<A*>(A *&&)' being compiled
with
[
_Ty=interpreter_context_ptr
]
source_file.cpp(68): note: see reference to function template instantiation 'void std::vector<interpreter_context_ptr,std::allocator<_Ty>>::emplace_back<A*>(A *&&)' being compiled
with
[
_Ty=interpreter_context_ptr
]
source_file.cpp(30): error C2061: syntax error: identifier 'T'
source_file.cpp(32): error C2061: syntax error: identifier 'T'


I try their suggestion of putting
typename
everywhere before the
T
's, it doesn't help anything, and I don't get any more informative error messages.




What's the story here? What does the C2061 "identifier" error mean here, it means that MSVC can't keep track of the template parameter inside of the lambda body, or it is parsing it wrong and thinks
T
is a variable and not a type or something?

Is it impossible to refer to template parameters within a lambda which is inside a template function like this MSVC 2015?

Do I just have to factor out the lambdas and make more static template functions instead?




It turns out that if I just completely get rid of the lambdas and use template functions like so, then MSVC will compile it fine:

class interpreter_context_ptr {
void * object_;
void (*new_text_call_)(void *, const std::string &);
void (*error_text_call_)(void *, const std::string &);
void (*clear_input_call_)(void *);

template <typename T>
struct helper {
static void new_text(void * o, const std::string & str) {
static_cast<T*>(o)->new_text(str);
}
static void error_text(void * o, const std::string & str) {
static_cast<T*>(o)->error_text(str);
}
static void clear_input(void * o) {
static_cast<T*>(o)->clear_input();
}
};

public:
void new_text(const std::string & str) const {
this->new_text_call_(object_, str);
}

void error_text(const std::string & str) const {
this->error_text_call_(object_, str);
}

void clear_input() const { this->clear_input_call_(object_); }

template <typename T>
explicit interpreter_context_ptr(T * t)
: object_(static_cast<void *>(t))
, new_text_call_(&helper<T>::new_text)
, error_text_call_(&helper<T>::error_text)
, clear_input_call_(&helper<T>::clear_input) {
}
};


But as I wrote in comments, I'm pretty surprised if I actually have to go this far. The MSVC developers claim to have fully supported and implemented lambda functions on this C++11 feature page. I would think that should include being able to refer to ambient template parameters in the scope of the lambda.

Answer

At the time of this question, the Visual C++ 2015 compiler has trouble with operator+ on lambda's. Removing the + signs in front of each of your lambda's lets this compile. It does slightly alter the semantics (the effective type of the expression is slightly different), although I don't think it matters here.

Make sure you have the latest updates installed, to ensure no other compiler/library bugs make you lose time for no reason.