【问题标题】:C++ find encompassing typeC++ 查找包含类型
【发布时间】:2016-12-05 15:50:50
【问题描述】:

动机

在某种程度上,其动机是模仿 C++ 中的数学概念,主要是为了允许编写极其通用的算法。稍后会详细介绍动机。

首先,一些定义。

定义

  1. 如果B 可转换为A,则类型A 包含另一种类型B,至少几乎不会丢失信息*。这类似于超集B ⊆ A 的数学概念。
  2. 如果C 包含C X C,则C 类型在operatorX 下闭合。
  3. operatorX 下的A 类型的闭包C 是包含A 并在operatorX 本身下关闭的类型。 C 可能不是唯一的。 C 可能不存在。
  4. ABoperatorXoperatorY 下的闭包 C 是包含 AB 的类型,并且在 operatorX 和 @98765434 下关闭.这需要定义A X BB Y A 等。

*注意:这是一个相当不精确的说法,但如果没有很多繁琐的限制,很难给出严格的定义。例如:int 包含 chardouble 包含 floatdouble 包含 int32_t,因为它具有 52 位精度,但 float 不包含 int32_t,因为它只有23 位精度。

问题

给定两种类型 TU,都定义了 operator+operator*。如果找不到它们的闭包或发出错误,什么是有效的方法?

请注意,它们的运算符应该被认为是绝对疯狂的,也就是说,T 甚至可能不会在 operator+ 下关闭,Σ(k ∈ [0, n)) T 可能是依赖于 n 的类型。

例如,如果我们只想要operator+下的闭包

closure<unsigned, unsigned char>::type  //unsigned

可以很容易地实现为

template<typename T, typename U> struct closure {
    using type = decltype(std::declval<T>() + std::declval<U>());
};

但这并不适用于所有类型,因为如上所述,decltype(std::declval&lt;T&gt;() + std::declval&lt;U&gt;()) 可能不会关闭。

更多关于动机

假设某个通用算法需要处理多种不同类型。如果我们要创建一个变量来存储中间值,它的类型应该是它们的闭包。

最简单的示例是将int 添加到float,使用double 作为中间存储。但在这种情况下,语言实际上指定 floatintfloat 的闭包,这在某些情况下是次优的。

现在由于算法是通用的,我们需要一些方法来找到闭包,而无需事先知道这些类型及其运算符是什么。

我可能有点得意忘形,实际上创造了一些愚蠢的东西。

标题注释

我不能称它为“找到类型的闭包”,闭包通常意味着编程中的其他东西 :P 。如果有人建议一个更好的名字,我很乐意改变它。

【问题讨论】:

  • 你想如何处理上溢/下溢? int 在技术上甚至不会在 operator+(int,int) 下关闭,除非您明确地将加法视为对整数大小取模。
  • @mindriot 有符号整数溢出是未定义的行为 ;)
  • @mindriot 这也是其背后动机的一部分。我认为operator+ 下的int 的理想关闭实际上类似于long long 或大于int 的任何东西
  • 是的,这使得它非常困难。分析所有值都是int32_ts 的(a+b)+c 怎么样?第一个操作的闭包产生int64_t,因此如果没有关于可能范围的更多信息,第二个闭包将不得不产生int128_t,即使我们知道这不是必需的。所以,没有传递性。
  • @mindriot 实际上,如果我们在编译时知道只有三个添加,那么它们的闭包实际上就是int64_t。这当然永远无法解决运行时问题,甚至可能无法解决一些编译时问题。这是防止事故发生的另一层检查。此外,这不仅仅是溢出错误。

标签: c++ algorithm generics


【解决方案1】:

要对您的闭包所需的表示大小进行相当严格的估计,您必须至少跟踪您的操作可能需要的有效数字。以下仅给出整数类型的示例,并且仅涵盖加法。减法和乘法应该很容易做到;之后它会变得比现在更混乱。 :)

#include <iostream>
#include <limits>
#include <typeinfo>

namespace closure {

// A subset of numeric_limits provides, just to shorten stuff.  Tells us all we
// want to know about the properties of a particular integer representation.
template <typename T>
struct repr
{
  static constexpr bool is_signed = std::numeric_limits<T>::is_signed;
  static constexpr int digits = std::numeric_limits<T>::digits;
};

// An estimate of the range of the sum of two integers
template <typename R1, typename R2>
struct add
{
  static constexpr bool is_signed = R1::is_signed | R2::is_signed;
  // Can use std::max() when on C++14 or newer
  static constexpr int digits = ((R1::digits > R2::digits) ? R1::digits : R2::digits) + 1;
};

// Now the mess: map the required number of significant digits back to existing
// types.  Note that the edge case for two's complement is overestimated to
// preserve my personal sanity: e.g., a char covers -128..+127, but we will
// place -128 into an int16_t.
template <int digits, bool is_signed>
struct result_impl;

// Define unsigned types in terms of signed ones
template <int digits> struct result_impl<digits, false>
{
  using type = typename std::make_unsigned<typename result_impl<digits-1, true>::type>::type;
};
template <> struct result_impl<0, false> { using type = uint8_t; };

// Construct the correct type based on the number of needed significant bits.
// binary log
constexpr int log2(int x)
{
  return (x > 1) ? (log2(x>>1)+1) : 0;
}

// The required type based on the binary logarithm of the number of significant bits
template <int logdigits>
struct logtype;

template <> struct logtype<0> { using type = int8_t;  };
template <> struct logtype<1> { using type = int8_t;  };
template <> struct logtype<2> { using type = int8_t;  };
template <> struct logtype<3> { using type = int16_t; };
template <> struct logtype<4> { using type = int32_t; };
template <> struct logtype<5> { using type = int64_t; };

// And this is the actual type for signed integers with a certain minimum number of bits
template <int digits> struct result_impl<digits, true>
{
  using type = typename logtype<log2(digits)>::type;
};

// Finally, our result type using the representation types from above.
template <typename R>
struct result { using type = typename result_impl<R::digits, R::is_signed>::type; };

}

int main()
{
  using namespace closure;

  // Adding two 16-bit values should require a 32-bit type
  std::cout << typeid(result<add<repr<uint16_t>,
                                 repr<uint16_t>>>::type).name() << std::endl;

  // Adding three 16-bit values should still require a 32-bit type
  std::cout << typeid(result<add<add<repr<uint16_t>,
                                     repr<uint16_t>>,
                                 repr<uint16_t>>>::type).name() << std::endl;

  // Adding two 16-bit values, one signed, should require a signed 32-bit type
  std::cout << typeid(result<add<repr<uint16_t>,
                                 repr<int16_t>>>::type).name() << std::endl;

  // Adding a signed 16-bit and an unsigned 32-bit value, should require a signed 64-bit type
  std::cout << typeid(result<add<repr<uint32_t>,
                                 repr<int16_t>>>::type).name() << std::endl;
}

编辑:感谢@PasserBy,简化了有效二进制位数与所选整数大小之间的关系。)

通过c++filt -t 编译和运行结果应该会给你类似

unsigned int
unsigned int
int
long

作为输出。这并不多,但也许它是您正在寻找的起点。当涉及到浮点表示时,我担心它会变得更加可怕。

【讨论】:

  • 有一个简写:使用 constexpr log2,递归编写。这实际上非常好,但它不够通用。 C++ 可能缺少必要的工具,但如果我们实现了诸如对称群之类的代数结构,这应该仍然有效。
  • 哦,是的。哦!谢谢。
  • 我担心它可能无法变得更通用。我猜,您必须为您感兴趣的每个运算符定义每个运算符的行为(以及由此产生的所需最大范围)。
  • 我想主要问题是你期望你的最终结果能够做到什么。你有用例吗?一方面,您可以轻松地使用我所写的内容作为更复杂、带注释的数值类型的基础,这些类型实现您想要的运算符和函数并相应地缩放它们的输出。
猜你喜欢
  • 2011-05-04
  • 1970-01-01
  • 2020-02-25
  • 2012-12-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多