Linker complains about undefined references while compiling with Clang
Asked Answered
G

1

10

I've encountered a really weird problem with clang.

When I compile my code with C++20 using gcc-11 everything is fine.

The problem appears when I try to compile it with C++20 and clang-14 (using clang-15/16/17 does not help).

Linker complains about undefined references to all class template instantiations for containers, below is e.g. one of the error messages:

/opt/compiler-explorer/gcc-11.2.0/lib/gcc/x86_64-linux-gnu/11.2.0/../../../../x86_64-linux-gnu/bin/ld: /tmp/example-ded8d6.o:(.rodata._ZTV6WidgetINSt7__cxx114listIlSaIlEEEE[_ZTV6WidgetINSt7__cxx114listIlSaIlEEEE]+0x10): undefined reference to `Widget<std::__cxx11::list<long, std::allocator<long> > >::doSth()'
/opt/compiler-explorer/gcc-11.2.0/lib/gcc/x86_64-linux-gnu/11.2.0/../../../../x86_64-linux-gnu/bin/ld: /tmp/example-ded8d6.o:(.rodata._ZTV6WidgetISt6vectorIbSaIbEEE[_ZTV6WidgetISt6vectorIbSaIbEEE]+0x10): undefined reference to `Widget<std::vector<bool, std::allocator<bool> > >::doSth()'

So far I've managed to work around it by changing Widget's doSth signature from

void doSth() override

to:

__attribute__((used)) void doSth() override

but this is - as I said above - rather a workaround, not the real solution. Does anyone have an idea why Clang can't handle this?

Below are links to Compiler Explorer and C++ insights and a minimum reproducible example, extracted from my code base and reduced to an absolute minimum that will allow to reproduce the example and show my intentions.

Compiler explorer: https://godbolt.org/z/83dMMvozn

C++ insights: https://cppinsights.io/s/e35151b2

Update

I removed some parts of the code that were not needed, and added another (acceptable) workaround - instead of using lambda as an argument for "invoke" call I used callable struct - it "magically" worked - however, I do not understand why it wasn't working with lambda.

#include <memory>
#include <type_traits>
#include <variant>
#include <vector>
#include <list>

struct EnumPlaceholder
{
};

template <typename T>
struct ChannelTypeIdentity
{
    using Type = T;
};

template <template <typename, typename> typename T>
struct ContainerTypeIdentity;

template <>
struct ContainerTypeIdentity<std::vector>
{
    template <typename U>
    using Type = std::vector<U>;
};

template <>
struct ContainerTypeIdentity<std::list>
{
    template <typename U>
    using Type = std::list<U>;
};

enum class ValueType
{
    eUndefined,
    eBool,
    eUInt8T,
    eUInt16T,
    eUInt32T,
    eUInt64T,
    eInt8T,
    eInt16T,
    eInt32T,
    eInt64T,
    eFloat,
    eDouble,
    eEnum,
    eString
};

enum class ContainerType
{
    eNone,
    eVector,
    eList
};

using ChannelTypeMapperReturnType = std::variant<
    ChannelTypeIdentity<EnumPlaceholder>,
    ChannelTypeIdentity<uint8_t>,
    ChannelTypeIdentity<uint16_t>,
    ChannelTypeIdentity<uint32_t>,
    ChannelTypeIdentity<uint64_t>,
    ChannelTypeIdentity<int8_t>,
    ChannelTypeIdentity<int16_t>,
    ChannelTypeIdentity<int32_t>,
    ChannelTypeIdentity<bool>,
    ChannelTypeIdentity<float>,
    ChannelTypeIdentity<double>,
    ChannelTypeIdentity<std::string>
>;

using ContainerTypeMapperReturnType = std::variant<
    ContainerTypeIdentity<std::vector>,
    ContainerTypeIdentity<std::list>
>;

ChannelTypeMapperReturnType simulateChannelTypeMapperReverseMap()
{
    return ChannelTypeMapperReturnType(ChannelTypeIdentity<int32_t>());
}

ContainerTypeMapperReturnType simulateContainerTypeMapperReverseMap()
{
    return ContainerTypeMapperReturnType(ContainerTypeIdentity<std::vector>{});
}

template <typename Func, typename ChannelTypeId>
constexpr auto invoke(Func&& func, ChannelTypeId channelTypeId)
{
    const auto valueType = simulateChannelTypeMapperReverseMap(); // simulated value, normally lookup in mapper

    if (channelTypeId.containerType != ContainerType::eNone)
    {
        return std::visit(
            [&channelTypeId, func = std::forward<Func>(func)](const auto& valueIdentity) mutable {
                using ValueType = typename std::decay_t<decltype(valueIdentity)>::Type;
                return std::visit(
                    [func = std::forward<Func>(func)](const auto& containerIdentity) {
                        using Type = typename std::decay_t<decltype(containerIdentity)>::template Type<ValueType>;

                        return func(ChannelTypeIdentity<Type>{});
                    },
                    simulateContainerTypeMapperReverseMap()); // simulated value, normally lookup in mapper
            },
            valueType);
    }

    return std::visit(std::forward<Func>(func), valueType);
}

struct ChannelTypeId
{
    ValueType valueType;
    ContainerType containerType;
    friend auto operator<=>(const ChannelTypeId&, const ChannelTypeId&) = default;
};

class IWidget
{
public:
    IWidget() = default;

    virtual void doSth() = 0;

    virtual ~IWidget() = default;
};

template <typename T>
class Widget : public IWidget
{
public:
    void doSth() override
    {

    }
};

template <typename T>
class Factory
{
public:
    static std::unique_ptr<IWidget> create()
    {
        return std::make_unique<Widget<T>>();
    }
};

struct Callable
{
    template <typename T>
    auto operator()(const T& identity) const
    {
        using ValueType = typename T::Type;
        return Factory<ValueType>::create();
    }
};

std::unique_ptr<IWidget> doSth(ChannelTypeId id)
{
    // This does not work - linker complains
    return invoke(
        []([[maybe_unused]] const auto& identity) -> std::unique_ptr<IWidget> {
            using ValueType = typename std::decay_t<decltype(identity)>::Type;
            return Factory<ValueType>::create();
        
    }, id);

    // This works
    // return invoke(Callable(), id);
}

int main() 
{ 
    auto ptr = doSth(ChannelTypeId{ 
        .valueType = ValueType::eUInt32T, 
        .containerType = ContainerType::eVector
    });

    ptr->doSth();
}
Gelhar answered 13/2 at 18:10 Comment(9)
dump() and #include <source_location> can be removed. Further code reducing for getting a minimal reproducible example is desired.Austen
dump function was for the purpose of showing that the code does what was intended, however, I removed it as you suggested. Moreover, I added one more workaround - I replaced the lambda with a callable object - the question is why the lambda was causing the problem?Gelhar
There is no such linker here with clang 14 and c++20. See demoLitho
Check once again, I left the working code with workaround and commented out the part that was causing the issue. Now I reverted it to the failing version. You can play with it by commenting and uncommenting code in doSth functionGelhar
This must be a joke that somebody has closed this question. The code has been reduced to the absolute minimum, the description clearly says what works and what doesn't. Where are you going Stack Overflow?Gelhar
It could be just enum class ValueType { eUInt32T, eInt32T }; and minus 20+ lines of the code.Austen
Yeah, and then somebody would ask - "why do you need enum with just two options - you could have done it a different way". Really, 20 lines of code where the lines define just enum values don't really affect the readability but show the author's intentions.Gelhar
@Gelhar Always show the code that reproduces the error and not the code the "can" reproduce the error.Litho
Reduced failcases: source file && preprocessed + creduce. This looks like an LLVM bug. On their bugtracker, they have a few reports of missing vtable symbols when lambdas and class with virtual members are used together. Possibly related: #45298, #60965, #57561Backgammon
P
0

My suggestion (which is admittedly just another workaround, given this is a reported compiler bug), is to force each instantiation of Factory to be ODR-used, which can be achieved by removing the trailing return type of the lambda passed to invoke:

return invoke(
    []([[maybe_unused]] const auto& identity) {
        using ValueType = typename std::decay_t<decltype(identity)>::Type;
        return Factory<ValueType>::create();
    
}, id);

This is not meant to imply that the instantiations aren't already ODR-used in this program, it's just that this particular compiler bug seems to be caused by Clang treating them like they aren't.

Prelacy answered 21/4 at 17:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.