6

Suppose I have a struct template S that is parametrized by an engine:

template<class Engine> struct S;

I have two engines: a "static" one with a constexpr member function size(), and a "dynamic" one with a non-constexpr member function size():

struct Static_engine {
    static constexpr std::size_t size() {
        return 11;
    }
};

struct Dynamic_engine {
    std::size_t size() const {
        return size_;
    }
    std::size_t size_ = 22;
};

I want to define size() member function in S that can be used as a constexpr if the engine's size() is constexpr. I write:

template<class Engine>
struct S {
    constexpr std::size_t size() const {
        return engine_.size();
    }
    Engine engine_;
};

Then the following code compiles with GCC, Clang, MSVC and ICC:

S<Static_engine> sta;         // not constexpr
S<Dynamic_engine> dyn;

constexpr auto size_sta = sta.size();
const auto size_dyn = dyn.size();

Taking into account intricacies of constexpr and various "ill-formed, no diagnostic is required", I still have the question: is this code well-formed?

Full code on Godbolt.org

(I tagged this question with both and in case this code has different validity in these two standards.)

1
  • "ill-formed, no diagnostic is required" There's not a lot of that regarding constexpr functions. If you try to misuse their constexpr status, compilers are required to be noisy. Commented Nov 14, 2019 at 22:15

3 Answers 3

7

The code is fine as written.

[dcl.constexpr]

6 If the instantiated template specialization of a constexpr function template or member function of a class template would fail to satisfy the requirements for a constexpr function or constexpr constructor, that specialization is still a constexpr function or constexpr constructor, even though a call to such a function cannot appear in a constant expression. If no specialization of the template would satisfy the requirements for a constexpr function or constexpr constructor when considered as a non-template function or constructor, the template is ill-formed, no diagnostic required.

The member may not appear in a constant expression for the specialization that uses Dynamic_engine, but as the paragraph above details, that does not make S::size ill-formed. We are also far from ill-formed NDR territory, since valid instantations are possible. Static_engine being a prime example.

The quote is from n4659, the last C++17 standard draft, and similar wording appears in the latest C++20 draft.


As for the evaluation of sta.size() as a constant expression, going over the list at [expr.const] I cannot find anything that is disallowed in the evaluation itself. It is therefore a valid constant expression (because the list tells us what isn't valid). And in general for a constexpr function to be valid, there just needs to exist some set of arguments for which the evaluation produces a valid constant expression. As the following example form the standard illustrates:

constexpr int f(bool b)
  { return b ? throw 0 : 0; }           // OK
constexpr int f() { return f(true); }   // ill-formed, no diagnostic required

struct B {
  constexpr B(int x) : i(0) { }         // x is unused
  int i;
};

int global;

struct D : B {
  constexpr D() : B(global) { }         // ill-formed, no diagnostic required
                                        // lvalue-to-rvalue conversion on non-constant global
};
Sign up to request clarification or add additional context in comments.

3 Comments

Would be helpful to include the example that this clause now has: eel.is/c++draft/dcl.constexpr#6
@Barry - I'm afraid I'm confused. This specific clause moved in numbering and is now p7. Is the example for the previous clause relevant here too? Genuine question.
They're pretty related. One's just for regular functions and one's for templates. Just generally, it's only ill-formed ndr if there does not exist a single formulation that is a constant expression. Otherwise, it's fine.
3

Yes.

Functions may be marked as constexpr without being forced to be evaluated at compile-time. So long as you satisfy the other requirements for marking a function as constexpr, things are okay (returns a literal type, parameters are literals, no inline asm, etc.). The only time you may run into issues is if it's not actually possible to create arguments that satisfy the function being called as a core constant expression. (e.g., if your function had undefined behavior for all values, then your function would be ill-formed NDR)

In C++20, we received the consteval specifier that forces all calls to the function to be able to produce a compile-time constant (constexpr).

2 Comments

consteval is rather stronger than that—it says that every call must produce a constant expression (perhaps indirectly).
@DavisHerring: Well said. Updated for clarity.
0

Not a direct answer but an alternative way:

struct Dynamic_Engine
{
  using size_type = size_t;

  size_type size() const
  {
    return _size;
  }

  size_type _size = 22;
};
struct Static_Engine
{
  using size_type = std::integral_constant<size_t, 11>;

  size_type size() const
  {
    return size_type();
  }
};

template <typename ENGINE>
struct S
{
  auto size() const
  {
    return _engine.size();
  }

  ENGINE _engine;
};

int main()
{
  S<Static_Engine>  sta;
  S<Dynamic_Engine> dyn;

  const auto size_sta = sta.size();
  const auto size_dyn = dyn.size();

  static_assert(size_sta == 11);
}

I had the same kind of problems and IMHO the easiest and more versatile solution is to use std::integral_constant. Not more needs to juggle with constexpr as the size information is directly encoded into the type

If you still really want to use constexpr (with its extra complications) you can do:

struct Dynamic_Engine
{
  size_t size() const
  {
    return _size;
  }

  size_t _size = 22;
};
struct Static_Engine
{
  static constexpr size_t size() // note: static 
  {
    return 11;
  }
};

template <typename ENGINE>
struct S
{
  constexpr size_t size() const
  {
    return _engine.size();
  }

  ENGINE _engine;
};

int main()
{
  S<Static_Engine>  sta;
  S<Dynamic_Engine> dyn;

  constexpr size_t size_sta = sta.size();
  const size_t size_dyn = dyn.size();

  static_assert(size_sta == 11);
}

3 Comments

The “static” choice need not in practice actually produce a single constant, so long as the object is usable in constant expressions its size() can be.
@DavisHerring thanks for the comment. Actually in my code I really use the std::integral_constant approach. For the static approach I have added inline that must fix that.
@DavisHerring, you are right plus I have misread your initial comment. I have removed all this. Sorry for the noise.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.