6

I've read answers to this question, peeked at gcc implementation for std::any, and didn't quite understand the implementation. However, I thought to try out the idea of using function pointers to deduce types. In debug I get expected results, but in release my template specializations are optimized away... I've tried to use #pragma optimize("", off) and __declspec(noinline) but it didn't help.

AFAIK comparing pointers to functions is not UB. So I guess that MSVC (in my case) optimizes template specializations away...

Is there a way to make this work?

#include <iostream>
#include <memory>

template <typename T>
void deduce_type()
{}


class any
{
    /*
    template <typename T, typename ... Args>
    any(Args&& ... args)
        : ptr_(new T(std::forward<Args>(args) ...), std::default_delete<T>())
    {}
    */

public:

    template <typename T>
    any(T&& other)
        : ptr_(new T(std::forward<T>(other))),
        deduce_type_(&deduce_type<T>)
    {}

    template <typename T>
    bool contains_type() const
    {
        return deduce_type_ == &deduce_type<T>;
    }

private:
    // std::unique_ptr<void, std::default_delete<void>> ptr_;
    void* ptr_;
    void(*deduce_type_)();

};

struct Foo
{

};


int main()
{

    any anyInt = 16;
    any anyInt2 = 17;
    any anyDouble = 2.0;
    any anyFoo = Foo();


    bool isInt = anyInt.contains_type<int>(),
        isInt2 = anyInt2.contains_type<int>(),
        notDouble = anyInt.contains_type<double>(), // 0 expected
        isDouble = anyDouble.contains_type<double>(),
        isFoo = anyFoo.contains_type<Foo>(),
        notFoo = anyInt.contains_type<Foo>(); // 0 expected

    std::cout << "is int = " << isInt << std::endl;
    std::cout << "is int = " << isInt2 << std::endl;
    std::cout << "is not double = " << notDouble << std::endl;
    std::cout << "is double = " << isDouble << std::endl;
    std::cout << "is Foo = " << isFoo << std::endl;
    std::cout << "is not Foo = " << notFoo << std::endl;

    return 0;
}

Release output:

is int = 1
is int = 1
is not double = 1
is double = 1
is Foo = 1
is not Foo = 1
foo is deducible = 1

Debug and expected output:

is int = 1
is int = 1
is not double = 0
is double = 1
is Foo = 1
is not Foo = 0
foo is deducible = 1

So according to the comments the problem is caused by the linker when Identical Code Folding is enabled (by default in Release in MSVC). I've found no ways to disable this linker flags in code for separate functions, so there is nothing to do but to invent some ugly workarounds that would prevent functions from being folded to one address. That is by forcing them somehow to generate "unique" instructions for each template instantiation...

When I change the signature of a template function to simply return an integer, I get desired results, but only when particular specializations have been explicitly provided...

template <typename T>
size_t deduce_type()
{
    return 0;
}

template <>
size_t deduce_type<double>()
{
    return 1;
}

template <>
size_t deduce_type<Foo>()
{
    return 2;
}

I get expected output in this case even if the same value is returned in different specializations. This, however, holds true only if specializations return different value then the basic template.

I guess I have to open another question...

Sergey Kolesnik
  • 3,009
  • 1
  • 8
  • 28
  • 6
    Checkout https://learn.microsoft.com/en-us/cpp/build/reference/opt-optimizations?view=msvc-160. Especially `/opt:icf`. It notes: _Because /OPT:ICF can cause the same address to be assigned to different functions or read-only data members (that is, const variables when compiled by using /Gy), it can break a program that depends on unique addresses for functions or read-only data members. For more information, see /Gy (Enable Function-Level Linking)._ – Mike Vine Jan 20 '21 at 14:58
  • Functions are not objects. It is clear that object have unique addresses, but it is not clear to me if this applies to functions as well. Is there a rule that all functions have unique pointer values, or can equivalent functions share the same pointer value? Edit : [This](https://timsong-cpp.github.io/cppwp/expr.eq#3.2) seems to indicate that the pointer values are unique, unless equivalent functions can be considered "the same function". – François Andrieux Jan 20 '21 at 15:00
  • @FrançoisAndrieux they are supposed to have unique addresses, otherwise a lot of things that use dynamic loading would not work (take opengl for instance). Moreover, this code does yield expected output with gcc -O3 https://ideone.com/zUAlOy – Sergey Kolesnik Jan 20 '21 at 15:42
  • @SergeyKolesnik There is a difference between be allowing unique addresses and requiring unique addresses. I have no doubt functions *can* have unique addresses. And this will probably always have to be the case for exported symbols. But in the case of this question every specialization of the function would have identical assembly. It would make sense to only generate the function once, if the standard allows it. My question is whether the standard allows it or not. – François Andrieux Jan 20 '21 at 15:49
  • @MikeVine if you could provide a cross platform version of my code that would prevent such optimisation, I would accept your answer. – Sergey Kolesnik Jan 20 '21 at 15:50
  • @SergeyKolesnik My take on Mike's comment is that you may be using a compiler flag that is causing the difference in behavior. If so, the fix would be to change how you compile your code. It wouldn't be a change to your code. – François Andrieux Jan 20 '21 at 15:52
  • @MikeVine is it possible to disable `icf` for a single function via `#pragma` directive? – Sergey Kolesnik Jan 20 '21 at 16:14
  • @FrançoisAndrieux suppose I want this behavior to be utilized by my library. And I don't want to enforce the users to compile their applications with particular linker optimizations disabled. So in terms of portability I think I need some workaround within the template function, to prevent its specializations from folding into one address... – Sergey Kolesnik Jan 20 '21 at 16:25
  • @SergeyKolesnik Maybe try adding `volatile T * unused = nullptr;` to the function. `volatile` has a tendency to impede optimizations. Even if it works though, there may be nothing preventing future compilers from working out that your function doesn't do anything. – François Andrieux Jan 20 '21 at 16:30
  • @FrançoisAndrieux it didn't help... I suppose I have to open another question to find out the way to prevent functions from folding... – Sergey Kolesnik Jan 20 '21 at 18:40
  • @FrançoisAndrieux: [expr.eq]/3 is pretty clear that pointers to different functions compare unequal, and [temp.type]/1 implies that different *template-id*s refer to different functions. – Davis Herring Jan 22 '21 at 00:33

1 Answers1

1

You can work around this non-conformance by using a variable template (or a static local variable—in which case you call the function in the constructor—or a static data member of a class template):

template<class T>
constexpr decltype(nullptr) tag{};

class any {
  void *p;
  const decltype(nullptr) *t;

public:
  template<class T>
  any(…) : p(…),t(&tag<T>) {}

  // contains_type much as before
};

This is of course basically std::type_info, but might be preferable if the other capabilities of that facility are unneeded.

Davis Herring
  • 36,443
  • 4
  • 48
  • 76