Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
138 views
in Technique[技术] by (71.8m points)

c++ - Compile time template instantiation check

Is it possible to check if a template type has been instantiated at compile time so that I can use this information in an enable_if specialization?

Let's say I have

template <typename T> struct known_type { };

Can I somehow define some is_known_type whose value is true if known_type is instantiated at compile time?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

It's possible to do this if you leverage the fact that specific expressions may or may not be used in places where constexprs are expected, and that you can query to see what the state is for each candidate you have. Specifically in our case, the fact that constexprs with no definition cannot pass as constant expressions and noexcept is a guarantee of constant expressions. Hence, noexcept(...) returning true signals the presence of a properly defined constexpr.

Essentially, this treats constexprs as Yes/No switches, and introduces state at compile-time.

Note that this is pretty much a hack, you will need workarounds for specific compilers (see the articles ahead) and this specific friend-based implementation might be considered ill-formed by future revisions of the standard.

With that out of the way...

User Filip Roséen presents this concept in his article dedicated specifically to it.

His example implementation is, with quoted explanations:

constexpr int flag (int);

A constexpr function can be in either one of two states; either it is usable in a constant-expression, or it isn't - if it lacks a definition it automatically falls in the latter category - there is no other state (unless we consider undefined behavior).

Normally, constexpr functions should be treated exactly as what they are; functions, but we can also think of them as individual handles to "variables" having a type similar to bool, where each "variable" can have one of two values; usable or not-usable.

In our program it helps if you consider flag to be just that; a handle (not a function). The reason is that we will never actually call flag in an evaluated context, we are only interested in its current state.

template<class Tag>
struct writer {
  friend constexpr int flag (Tag) {
    return 0;
  }
};

writer is a class template which, upon instantiation, will create a definition for a function in its surrounding namespace (having the signature int flag (Tag), where Tag is a template-parameter).

If we, once again, think of constexpr functions as handles to some variable, we can treat an instantiation of writer as an unconditional write of the value usable to the variable behind the function in the friend-declaration.

template<bool B, class Tag = int>
struct dependent_writer : writer<Tag> { };

I would not be surprised if you think dependent_writer looks like a rather pointless indirection; why not directly instantiate writer where we want to use it, instead of going through dependent_writer?

  1. Instantiation of writer must depend on something to prevent immediate instantiation, and;
  2. dependent_writer is used in a context where a value of type bool can be used as dependency.
template<
  bool B = noexcept (flag (0)),
  int    =   sizeof (dependent_writer<B>)
>
constexpr int f () {
  return B;
}

The above might look a little weird, but it's really quite simple;

  1. will set B = true if flag(0) is a constant-expression, otherwise B = false, and;
  2. implicitly instantiates dependent_writer (sizeof requires a completely-defined type).

The behavior can be expressed with the following pseudo-code:

IF [ `int flag (int)` has not yet been defined ]:
  SET `B` =   `false`
  INSTANTIATE `dependent_writer<false>`
  RETURN      `false`
ELSE:
  SET `B` =   `true`
  INSTANTIATE `dependent_writer<true>`
  RETURN      `true`

Finally, the proof of concept:

int main () {
  constexpr int a = f ();
  constexpr int b = f ();
  static_assert (a != b, "fail");
}

I applied this to your particular problem. The idea is to use the constexpr Yes/No switches to indicate whether a type has been instantiated. So, you'll need a separate switch for every type you have.

template<typename T>
struct inst_check_wrapper
{
    friend constexpr int inst_flag(inst_check_wrapper<T>);
};

inst_check_wrapper<T> essentially wraps a switch for whatever type you may give it. It's just a generic version of the original example.

template<typename T>
struct writer 
{
    friend constexpr int inst_flag(inst_check_wrapper<T>) 
    {
        return 0;
    }
};

The switch toggler is identical to the one in the original example. It comes up with the definition for the switch of some type that you use. To allow for easy checking, add a helper switch inspector:

template <typename T, bool B = noexcept(inst_flag(inst_check_wrapper<T>()))>
constexpr bool is_instantiated()
{
    return B;
}

Finally, the type "registers" itself as initialized. In my case:

template <typename T>
struct MyStruct
{
    template <typename T1 = T, int = sizeof(writer<MyStruct<T1>>)>
    MyStruct()
    {}
};

The switch is turned on as soon as that particular constructor is asked for. Sample:

int main () 
{
    static_assert(!is_instantiated<MyStruct<int>>(), "failure");
    MyStruct<int> a;
    static_assert(is_instantiated<MyStruct<int>>(), "failure");
}

Live on Coliru.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...