2

I'm implementing a constant/immutable class in C++ 20 and I'd like to make as much as possible constant and do some value checks in my factory functions.

Below is a simplified example (paste to godbolt.org) of what I'm trying to achieve. Please see the code comments on the options I'm considering.

Note that this is for an embedded project and using exceptions is not an option.

Any ideas on achieving good compile-time errors for bad values of "i" where the error msg. actually relates to to the value of i (without templates)?

Update We actually have some non constant uses of this class (where "i" is not constant) so it seems that consteval is out of the picture (together with the template case). Basically, the question is then: Is it possible to have compile-time errors if "i" is constant and runtime errors if "i" is not?

Update2 Updated code with option 5, based on comments below.

#include <cstdlib>

#define Assert while(true) {}

class ConstantClass
{
    public:

        static consteval ConstantClass ConstEvalFactory(int i)
        {
            if(i < 0)
            {
                // Option 1
                // - Use #error
                // - Always fails at compile time
                // - even for valid i

                // #error "Invalid value"

                // Option 2
                // - Non constant expression inside consteval
                // - Compile-time error msg. do not relate to the value of i

                Assert(false);
            }
            else
            {
                return ConstantClass(i);
            }
        }

        template<int i>
        static consteval ConstantClass TemplateConstEvalFactory() 
        {
            // Option 5
            // - template function with static assert
            // - user will have to choose which factory to call
            //   - I.e. x<...>() or x(...)
            static_assert(i>=0);

            return ConstantClass(i);
        }

        static constexpr ConstantClass ConstExprFactory(int i) 
        {
            if(i < 0)
            {
                // Option 3
                // - Non constant expression inside constexpr
                // - If i < 0, this will compile to non-constant and fail at runtime
                // - Failure will depend on implementation of "Assert"

                Assert(false);
            }
            else
            {
                return ConstantClass(i);
            }
        }

    private:
        constexpr ConstantClass(int i)
        {
            // Construction implementation
        }
};

template<int i>
class ConstantTemplateClass
{
    // Option 4
    // - static assert on template parameter
    // - Works but we'd prefer non templates in this case

    static_assert(i>=0);
};

int main()
{
    // Examples of invalid instances
    // ConstantClass MyC = ConstantClass::ConstEvalFactory(-1);
    // ConstantClass MyC = ConstantClass::TemplateConstEvalFactory<-1>();
    // ConstantClass MyC = ConstantClass::ConstExprFactory(-1);
    // ConstantTemplateClass<-1> MyC;
}
jgreen81
  • 705
  • 1
  • 6
  • 14
  • 2
    https://stackoverflow.com/questions/20461121/constexpr-error-at-compile-time-but-no-overhead-at-run-time https://stackoverflow.com/questions/8626055/c11-static-assert-within-constexpr-function . If `i` is constant, just make `i` a template parameter. – KamilCuk Feb 13 '23 at 12:24
  • @KamilCuk I actually specifically stated that I wanted to not use templates. However, your comment made me think of why. Besides the task of refactoring, I also realize that we actually have some non constant uses of the class (where "i" is not constant). So it seems that consteval is not really an option either. I guess the questions is then, is it possible to get a compile time error if "i" is constant and a runtime error when it is not? – jgreen81 Feb 13 '23 at 12:39
  • 1
    @jgreen81 - What about *two* constructors? One that receive the `i` value as template parameter, for const uses, that can be checked compile-time with a classic `static_assert()`, and one that receive `i` as a normal run-time parameter that can be checked run-time – max66 Feb 13 '23 at 13:20
  • @max66 Not a bad idea. One drawback is you'll have to choose between the factory functions when you create an instance (whether you want the constant one or not). – jgreen81 Feb 13 '23 at 13:49
  • @jgreen81 - Yes... you have to choose somewhere. – max66 Feb 13 '23 at 13:51
  • @max66 I've updated the post (code example) accordingly (option 5) – jgreen81 Feb 13 '23 at 13:56
  • @jgreen81, whether something is known at compile time or run time... is itself known at compile time, in the sense that everywhere in the code you need the constructor, you know if you are passing to it a compile-time known value or not, so you know which you have to call. (I'm referring to max66's proposal.) – Enlico Feb 13 '23 at 14:19

1 Answers1

1

Following my comment, I suggest a double-factory; a consteval one, where the argument is a template parameter and can be checked with a static_assert(), and another for the run-time cases where the argument can be checked run time.

The following is compiling (commenting the a2 declaration) example

#include <iostream>

class NotEverConstClass
{
  public: 
    template <int I>
    static consteval NotEverConstClass constFactory () 
    {
      static_assert( I >= 0, "some explicative error message 1" );

      return NotEverConstClass{I};
    }

    static NotEverConstClass runTimeFactory (int i)
    {
      if ( i < 0 )
        std::cout << "some explicative error message 2" << std::endl;

      return NotEverConstClass{i};
    }

  private:
    constexpr NotEverConstClass (int) 
    { }
};

int main()
{
  // compilation OK, runtime OK
  constexpr auto a0 { NotEverConstClass::constFactory<42>() };

  // compilation OK, runtime OK
  auto a1 { NotEverConstClass::runTimeFactory(42) };

  // compilation error with message "some explicative error message 1"
  // constexpr auto a2 { NotEverConstClass::constFactory<-42>() };
 
  // compilation OK, runtime error "some explicative error message 2" 
  auto a3 { NotEverConstClass::runTimeFactory(-42) };
}
max66
  • 65,235
  • 10
  • 71
  • 111