Skip to content

Instantly share code, notes, and snippets.

@johnmcfarlane
Last active October 13, 2017 20:03
Show Gist options
  • Save johnmcfarlane/c7dc07e179de56653272585adce70cb5 to your computer and use it in GitHub Desktop.
Save johnmcfarlane/c7dc07e179de56653272585adce70cb5 to your computer and use it in GitHub Desktop.
Draft proposal for addition of `std::constant`

Document number: DXXXXR0
Date: 2017-09-19
Reply-to: John McFarlane, cnl@john.mcfarlane.name
Reply-to: Louis Dionne, ldionne.2@gmail.com
Audience: SG6, SG14, LEWG

General-Purpose Constant Value Type

Introduction

A constant value type can make some arithmetic operations more efficient. For example, some low-level operations using the fixed-point type in [P0037] effectively become no-ops:

auto a = fixed_point<int, -10>{1};  // fixed-point wrapper around an int with resolution 2^-10
auto b = a << 1;  // fixed_point<int, -10>{2}, requires a run-time shift operation
auto c = a << integral_constant<int, 1>{};  // fixed_point<int, -9>{2}, bitwise equal to a; only the type has changed

Combined with a user-defined literal, this code can be made almost as terse and readable as the run-time code:

auto d = a << 1c;

Many other examples exists of situations where the value of an argument might affect the type of a result:

// snug returns the narrowest type that can hold the given value
auto e = snug(100);     // sizeof(e) == sizeof(int), snug cannot determine type from value
auto f = snug(100c);    // sizeof(f) == sizeof(int8_t), but it can determins type from type

This feature would also interact well with class template argument deduction:

auto g = fixed_point(0x100);    // fixed_point<int, 0>{256}, type cannot be determined based on initial value
auto h = fixed_point(0x100c); // fixed_point<int, 8>{256}, 8 fewer bits are devoted to low-order bits

Currently, the only standard type for expressing a constant value types is integral_constant which has drawbacks:

  1. It requires that two template parameters be specified where template<auto> would mean only one was necessary.

  2. If future revisions to the standard relax restrictions on non-type template parameters, it will be ill-named and ill-prepared.

  3. A lack of operator overloads means that results of many operations are values, e.g.:

    auto g = integral_constant<int, 2>{} + integral_constant<int, 2>{};
    // result is 4, not integral_constant<int, 4>{}

We propose a replacement for integral_constant called constant and an accompanying user-defined literal which address the above issues.

Prior Art

A similar proposal to improve on integral_constant was made in P0377. It addresses the first of the three drawbacks listed above: namely eliminating integral_constant's type template parameter. But it does not address the other two.

User-defined literals returning an constant value type can be found in multiple libraries and on forums including in [Boost.Hana] and [CNL].

Details

Definition of constant

template<auto Value>
struct constant {
    using value_type = decltype(Value);
    static constexpr value_type value = Value;

    constexpr operator value_type() const {
        return value;
    }
};

The implicit conversion operator ensures that constant objects are as easy to use as the variable they mimic. In the following example, the result of != is constant<false> which is then implicitly converted to bool:

static_assert(constant<1>{} != constant<2>{});

Operator Overloads

A complete set of unary and binary operators ensure that operations taking only constant operands do not return non-constant results:

// unary operator @
template<auto Value1>
constexpr auto operator@(constant<Value1> rhs) noexcept {
    return constant<@ Value1>{};
}

// binary operator @
template<auto Value1, auto Value2>
constexpr auto operator@(constant<Value1> lhs, constant<Value2> rhs) noexcept {
    return constant<Value1 @ Value2>{};
}

User-Defined Literals

A user-defined literal returns constant objects of values of type, maxint_t.

namespace literals {
    template<char ... Chars>
    constexpr auto operator "" c() noexcept;
}

The input string could contain whole numbers in a variety of bases:

using namespace literals;
auto i = 1000000000000c;  // constant<1000000000000LL>
auto j = 0x401c;    // constant<1025LL>
auto k = 0b10000c;  // constant<16LL>
auto l = 077;  // constant<63LL>

Discussion

The use of a pack of char as input to operator "" c is limiting. However, there is currently no better alternative. Future language revisions may allow some improvements:

  • fractional values, e.g. with a decimal place;
  • non-string input and
  • greater range than intmax_t.

Care should be taken to ensure the current design does not preclude these possibilities.

Reference Implementation

A simple proof of concept is available on [CompilerExplorer].

@RandomDSdevel
Copy link

@johnmcfarlane: 'c,' the literal suffix you chose, is a hexadecimal digit, so you can't use it (I noticed this when looking at the assignment to auto h.)

@MikeGitb
Copy link

MikeGitb commented Oct 7, 2017

Except for function overloading I don't quite understand, where the advantage of a constant_integaral type is compared to a simple integer that is known at compile time.

@johnmcfarlane
Copy link
Author

@RandomDSdevel: very good point, thanks. I'll raise that in the published paper.

@johnmcfarlane
Copy link
Author

@MikeGitb: it is not currently possible to determine a type from a value unless that value is a non-type template parameter.

For example, say I wanted to return a type that snugly fits a given value. Values less than 256 will fit in a byte but greater values require a wider type. Unfortunately, the following code cannot return both a uint8_t and an int. It must return an object of the same type in both cases.

constexpr auto snug(unsigned x) {
  if (x < 256) {
    return uint8_t{x};
  }
  else {
    return x;
  }
}

The value upon which the type depends must be part of the type of the function -- not just a value passed in:

template<unsigned X>
constexpr auto snug() 
{
  return std::conditional_t<X<=255, uint8_t, int>{X};
}

But the usage is unwieldy:

static_assert(identical(snug<255>(), uint8_t{255}));
static_assert(identical(snug<256>(), 256));

There are many other applications of this feature. But this one demonstrates how it can do something that merely passing by value cannot.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment