I had a random thought while drinking my Friday coffee. This is not a proposal. I discussed it on a C++ Discord, but it was met with criticism. That said, sometimes even the biggest failures can lead to interesting successes, so I'm curious what the std-discussion list thinks about it.

The concept is compile-time data, which I'm calling "consteval members". Its goal is to simplify some of the situations where templates and macros are abused by letting people express their intent in a more literal, OOP fashion using data that the compiler cannot allow in the actual assembly. This can be used as better control flow based on the specific value of literals, and it can be used for better compile-time error handling.

The key is that the transforms must happen at compile-time. The consteval data cannot be in the application at runtime.

Some concerns:

On the plus side, I also believe that it allows very readable and maintainable code, especially for library authors that hide its usage with access specifiers, so it might lead into some interesting ideas.

This email will show a use case where a factory-style object collects properties and produces an object instance ("the builder pattern"). This mechanism will allow the various functions to guide the user (with compiler errors) if they forgot required data, or if they set incompatible properties. In this case, the user must supply an IP address and a port, but they are allowed to choose any mechanism that does that, and (in this example) they can do so in any order (although another use case could easily check for order).

class BuildableEndpoint {
  consteval bool hasAddress; //Cannot be used at runtime. Does not appear in runtime object.
  consteval bool hasPort; // Cannot be used at runtime. Does not appear in runtime object.

  std::unique_ptr<InternetAddress> address;
  uint16_t port;

  // I'm guessing constexpr qualifier would be necessary if the argument affected the consteval members.
  BuildableEndpoint& AddIpV6AddressFromString(std::string argAddress)
  {
    static_assert(hasAddress == false, "Endpoint was already assigned an IP address.");
    address = ParseIpV6Address(argAddress);
    hasAddress = true;
    return *this;
  }

  BuildableEndpoint& AddIpV4AddressFromString(std::string argAddress)
  {
    static_assert(hasAddress == false, "Endpoint was already assigned an IP address.");
    address = ParseIpV4Address(argAddress);
    hasAddress = true;
    return *this;   
  }

  BuildableEndpoint& AddIpV4AddressFromMask(uint8_t A, uint8_t B, uint8_t C, uint8_t D)
  {
    static_assert(hasAddress == false, "Endpoint was already assigned an IP address.");
    address = std::make_unique<SomeIpV4Address>(A, B, C, D);
    hasAddress = true;
    return *this;
  }

  BuildableEndpoint& AddPort(uint16_t argPort)
  {
    static_assert(hasPort == false, "Endpoint was already assigned a port.");
    port = argPort;
    hasPort = true;
  }

  MyEndpoint Build()
  {
    static_assert(hasAddress, "Endpoint must have an address before building."); //OK
    static_assert(hasPort, "Endpoint must be assigned a port before building."); //OK

    return MyEndpoint(address, port);
  }
};

int main()
{
// This is okay. It has both an address (IPv4 or IPv6 doesn't matter) and a port.
  MyEndpoint endpoint1 = BuildableEndpoint()
    .AddIpV4AddressFromString("10.45.20.12")
    .AddPort(1022)
    .Build();

// This is also okay.
  MyEndpoint endpoint2 = BuildableEndpoint()
    .AddIpV4AddressFromMask(10, 45, 20, 12)
    .AddPort(1022)
    .Build();

// This will trigger the hasPort static assert in Build() and not compile.
//  MyEndpoint endpoint = BuildableEndpoint()
//    .AddIpV4AddressFromMask(10, 45, 20, 12)
//    .Build();
}