llvm / llvm-project

The LLVM Project is a collection of modular and reusable compiler and toolchain technologies.
http://llvm.org
Other
28.08k stars 11.59k forks source link

Code for virtual function not generated #45298

Open Erlkoenig90 opened 4 years ago

Erlkoenig90 commented 4 years ago
Bugzilla Link 45953
Version unspecified
OS Linux
Attachments Test code
CC @Erlkoenig90,@zygoloid

Extended Description

For the attached C++17 code involving Boost.Beast, clang++ gives an "undefined symbol" error (full message below). It appears that the code for the function boost::beast::stable_async_base<...>::before_invoke_hook() is not generated, leading to the error. It works with g++.

The error occurs with the LLVM 10.0.0 prebuilt binary for Ubuntu x86_64 and using the Boost library version 1.73.0, on Linux Mint 19.3. It happens with both LD and LLD. The Boost libraries were compiled with clang as well, but I think this is not really relevant as the missing function is a template and should be generated when compiling the application.

The code is compiled with clang++ test.cpp -fuse-ld=lld -lboost_system -pthread -lssl -lcrypto.

This is the missing function: https://github.com/boostorg/beast/blob/boost-1.73.0/include/boost/beast/core/async_base.hpp#L193

The attached code of course makes zero sense, but appears to be the minimal amount that reproduces the problem. It is unnecessarily contrived and I have since been pointed to a better (and working) solution, but there may still be a bug in Clang/LLVM that could be worth fixing.

The full linker error is:

ld.lld: error: undefined symbol: boost::beast::stable_async_base<void LoaderConnection::onResolve<int>(int&&, boost::system::error_code const&, boost::asio::ip::basic_resolver_results<boost::asio::ip::tcp> const&)::'lambda'(boost::system::error_code const&, unsigned long), boost::asio::executor, std::allocator<void> >::before_invoke_hook()
>>> referenced by test.cpp
>>>               /tmp/test-223f2a.o:(vtable for boost::beast::http::detail::write_msg_op<void LoaderConnection::onResolve<int>(int&&, boost::system::error_code const&, boost::asio::ip::basic_resolver_results<boost::asio::ip::tcp> const&)::'lambda'(boost::system::error_code const&, unsigned long), boost::beast::ssl_stream<boost::beast::basic_stream<boost::asio::ip::tcp, boost::asio::executor, boost::beast::unlimited_rate_policy> >, true, boost::beast::http::empty_body, boost::beast::http::basic_fields<std::allocator<char> > >)
>>> referenced by test.cpp
>>>               /tmp/test-223f2a.o:(vtable for boost::beast::stable_async_base<void LoaderConnection::onResolve<int>(int&&, boost::system::error_code const&, boost::asio::ip::basic_resolver_results<boost::asio::ip::tcp> const&)::'lambda'(boost::system::error_code const&, unsigned long), boost::asio::executor, std::allocator<void> >)

ld.lld: error: undefined symbol: boost::beast::detail::allocate_stable_state<boost::beast::http::serializer<true, boost::beast::http::empty_body, boost::beast::http::basic_fields<std::allocator<char> > >, std::allocator<void> >::~allocate_stable_state()
>>> referenced by test.cpp
>>>               /tmp/test-223f2a.o:(vtable for boost::beast::detail::allocate_stable_state<boost::beast::http::serializer<true, boost::beast::http::empty_body, boost::beast::http::basic_fields<std::allocator<char> > >, std::allocator<void> >)

ld.lld: error: undefined symbol: boost::beast::detail::allocate_stable_state<boost::beast::http::serializer<true, boost::beast::http::empty_body, boost::beast::http::basic_fields<std::allocator<char> > >, std::allocator<void> >::~allocate_stable_state()
>>> referenced by test.cpp
>>>               /tmp/test-223f2a.o:(vtable for boost::beast::detail::allocate_stable_state<boost::beast::http::serializer<true, boost::beast::http::empty_body, boost::beast::http::basic_fields<std::allocator<char> > >, std::allocator<void> >)

ld.lld: error: undefined symbol: boost::beast::detail::allocate_stable_state<boost::beast::http::serializer<true, boost::beast::http::empty_body, boost::beast::http::basic_fields<std::allocator<char> > >, std::allocator<void> >::destroy()
>>> referenced by test.cpp
>>>               /tmp/test-223f2a.o:(vtable for boost::beast::detail::allocate_stable_state<boost::beast::http::serializer<true, boost::beast::http::empty_body, boost::beast::http::basic_fields<std::allocator<char> > >, std::allocator<void> >)
clang-10: error: linker command failed with exit code 1 (use -v to see invocation)
ec04fc15-fa35-46f2-80e1-5d271f2ef708 commented 4 years ago

Slightly more cleaned up test case:

// 1) use_vtable() uses vtable for HasVtable<int>
template<typename T> struct HasVtable {
  virtual void please_define_me() { T::error; }
};
template<typename T = int> void use_vtable() { HasVtable<T> q; }

// 2) somehow trigger instantiation of use_vtable without
//    triggering instantiation of the vtable (???)
auto F = [](auto...) {
  [](auto...) -> decltype(use_vtable()) { use_vtable(); }();
};
template<typename T> auto call(T f) -> decltype(f());
using U = decltype(call(F));

// 3) emit a reference to use_vtable to observe the badness
int main() { use_vtable(); }

I'm not sure exactly what's going wrong in step 2, but ... something about this doubly-nested generic lambda instantiation happening within a SFINAE context seems to mess up the instantiation of use_vtable.

ec04fc15-fa35-46f2-80e1-5d271f2ef708 commented 4 years ago

Looks like the problem is triggered by the use of two_arg_handler_test from tcp::resolver::initiate_async_resolve::operator()'s static_assert. Reduced to:

template <typename T> struct Q {
  virtual void please_define_me() {}                                                            
};                                                                                              

template <typename T> void onResolve(T) {                                                       
  Q<T> q;                                                                                       
}                                                                                               

template <typename Handler>
auto two_arg_handler_test(Handler h) -> decltype(sizeof(h(), 0));                               

int main() {
  auto F = [](auto... args) { 
    [](auto... fwdLambdaArgs) -> decltype(onResolve(fwdLambdaArgs...)) {                        
      return onResolve(fwdLambdaArgs...);                                                       
    }(42, args...);                                                                             
  };                                                                                            

  static_assert(sizeof(two_arg_handler_test(F)) != 0);                                          
  F();                                                                                          
}
Erlkoenig90 commented 4 years ago

I added the preprocessed source. My source code is already quite reduced, that's why it's so weird... Indeed, changing small details makes it work. I think it has something to do with the combination of the lambda and its return type deduction, the "wrap" function and the fact that "onResolve" is a template (it works if it's not templated).

When I compile with -c and run llvm-nm on the generated .o file, I can see that the symbol is actually undefined ("U"). Other instantiations of the same functions DO exist however ("W").

Erlkoenig90 commented 4 years ago

Preprocessed test case

ec04fc15-fa35-46f2-80e1-5d271f2ef708 commented 4 years ago

Please can you attach preprocessed source code (https://llvm.org/docs/HowToSubmitABug.html#front-end-bugs)? (A reduced testcase would be great if you're interested in crafting one, but for a bug of this kind, correct reduction can be ... subtle.)

This looks likely to be a bug; it seems unlikely that there's an explicit instantiation of that function or key function or similar that would justify our not emitting this function template instantiation in every translation unit that has a use.