天天财汇 购物 网址 万年历 小说 | 三峰软件 小游戏 视频
TxT小说阅读器
↓小说语音阅读,小说下载↓
一键清除系统垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放,产品展示↓
首页 淘股吧 股票涨跌实时统计 涨停板选股 股票入门 股票书籍 股票问答 分时图选股 跌停板选股 K线图选股 成交量选股 [平安银行]
股市论谈 均线选股 趋势线选股 筹码理论 波浪理论 缠论 MACD指标 KDJ指标 BOLL指标 RSI指标 炒股基础知识 炒股故事
商业财经 科技知识 汽车百科 工程技术 自然科学 家居生活 设计艺术 财经视频 游戏--
  天天财汇 -> 设计艺术 -> constexpr是否是过度设计?既然条件是已知,算法在设计之初都要考虑到为什么不直接填结果? -> 正文阅读

[设计艺术]constexpr是否是过度设计?既然条件是已知,算法在设计之初都要考虑到为什么不直接填结果?

[收藏本文] 【下载本文】
在学习C++ constexpr中遇到一个问题,既然使用constexpr模板和元编程要求 入参是编译前已经确定,那为什么不直接考虑完所有的可能的场景…
fmt::format("{:3.01f}", value)
以上这句代码可以在编译期解析格式化字符串,生成数据校验代码。当格式化字符串错误,或者value无法被该方式格式化时自动报错。
现在代码里平均100行就有一条这类格式化日志,整个工程上千条日志。
全程手写校验,请。
说实话我没 get 到题主的点,所以回答可能也不完全准确。
有些东西是确定的,但不代表就适合自己算,比如阶乘 n!" role="presentation">n!n! 就没有查表和遍历之外的好方法, gcd(a,b)" role="presentation">gcd(a,b)\gcd(a, b) 需要做一个循环,如果是反三角函数之类的就更不适合自己算了。代码的超参数可能会变化,每次变化就要自己重新算然后复制粘贴肯定是不如编译器直接算的。所谓“考虑完所有的情形”纯粹是完美的想象,谁能保证自己真的能考虑完所有的情形,谁能保证几个月之后还要不要复用这段代码。
退一步讲,我们可以用 variadic template 实现循环展开,但是此时循环的上下界、步长必须是编译期间可知的量,比如给定常量 n" role="presentation">nn ,上界是 (n2)" role="presentation">(n2)\dbinom{n}{2} ,如果你手动维护 (n2)" role="presentation">(n2)\dbinom{n}{2} 的计算(外部计算然后复制过来),就有 n" role="presentation">nn 和 (n2)" role="presentation">(n2)\dbinom{n}{2} 取值不一致的潜在危险,毕竟谁都有可能粗心忘记改某个数值。
能几乎不改变什么 C++ 语法就实现编译期化简,自然也没必要额外引入其他的什么预处理,而且 constexpr 函数接收非 constexpr 参数时也能放到运行期计算,泛用性也好,一个定义处理所有情形。
我不是做 C++ 的,对于 C++23 也不熟悉,但我知道 Rust 里有类似的现象:编译期间可以将 JSON 文件的内容当作静态常量处理,本质也是编译期间读取文件内容,而非编译完之后在 runtime 读取文件,相当于省去从 JSON 里把数据复制粘贴到代码里的步骤,这种情况如果能够实现,也是非常方便设置超参数的。
C++发明constexpr函数是在C++11,而当初由于极度保守(考虑到它是十五年前发明的,那时候Windows7才发布),本身只是试水而已。到了C++17,发明constexpr if之后,C++标准实际上已经重新确定了constexpr函数的定位:编译期计算,例如我之前写的寻找deque最佳块大小的函数:

inline constexpr std::size_t calc_block_size(std::size_t const pv) noexcept
{
    // 块的基本大小
    auto constexpr base = 4096uz;
    // 基本大小下,元素最大大小,至少保证16个
    auto constexpr sieve = base / 16uz;

    if (pv < sieve)
    {
        // 在基本大小的1-8倍间找到利用率最高的
        auto block_sz = 0uz;
        auto rmd_pre = std::size_t(-1);
        auto result = 0uz;
        for (auto i = 0uz; i != 8uz; ++i)
        {
            block_sz = base * (i + 1uz);
            auto rmd_cur = (block_sz * i) % pv;
            // 越小利用率越高
            if (rmd_cur < rmd_pre)
            {
                rmd_pre = rmd_cur;
                result = block_sz;
            }
        }
        return result;
    }
    else
    {
        // 寻找y使得y大于16*元素大小,且y为4096整数倍
        return (pv * 16uz + base - 1uz) / base * base;
    }
}

这个函数需要你输入deque的value_type的大小,返回合适的块大小,函数的调用发生在deque成员函数的内部,就算可以算出来大小,你也没办法填进deque的成员函数里,并且因为deque的value_type的大小可以是任意大,你也不可能去在代码里用一张几亿行的表去枚举它的结果。
到了C++20时期,委员会逐渐认为仅仅做编译期计算已经满足不了胃口了,因此继续扩展constexpr函数的应用范围,constexpr函数不仅可以编译期用,还要运行期用,这样可以避免维护两套(一套编译期一套运行期)代码,同时纯编译期计算划分给consteval函数了。因此在C++23时,标准几乎允许任何函数标记为constexpr(除了协程),同时C++20、C++23、C++26也逐渐给几乎所有标准库函数添加了constexpr,扫清了维护一套代码的阻碍。
你这纯粹是想太多。
为什么会有constexpr函数?不就是人们发现一些运行期的计算能够放到编译期,以节省运行期时间。
你想把源码里的所有constexpr函数结果都算出来,再填进去?你乐意做这些重复性工作,当然是没问题的。一旦函数的实现修改,你得把所有结果重新算,再填进去。
假设我需要自定义一个array类,它有类型参数T,一个长度参数int。

template<typename T, int S>
class Array;

初始化的时候,我得知道需要多大的空间,例如我需要Array<int, 20>,那我就需要80字节,如果是Array<Double, 30>,那么需要240字节。
注意这里的T是类型参数,你根本不知道真实使用的时候到底是什么,用户给你来个Array<Panda, 100000>完全有可能,但是你根本不清楚这个Panda到底是啥。
那怎么知道我要申请多大的内存呢?
使用int size = sizeof(T) * S,然后调用malloc当然是可以的(有时可能不好用new,因为你不知道类型T有没有重载new操作符)。
但是这个语句可能会生成个乘法指令对吧。
所以constexpr int size = sizeof(T) * S,把这个运算放到编译期里面去,可能会更好一些。
为了代码的可读性和可复用性。常量确实是要求编译期已知,所以这个人确实可以让人预先算出来,但是如果计算过程复杂或者计算结果反人类,那么constexpr就是一个好选择。
比如说,我的代码有一个参数是某个时间的时间戳,也就是从时间元年到现在的秒数,但是这个时间戳经常会因为这份代码在不同位置要发生改动,都是编译期的常量,都可以通过计算得到,但是这个计算过程反人类。比如我有个同事需要调用我的接口,他看见我传递了一个魔鬼数字,搁那研究半天才发现是一个时间戳,又花费了半天去计算出来了他需要的时间点的时间戳去填这个常量(包括但不限于手动计算,写简易程序计算,在线时间戳工具装换)等等。
上面这个场景的问题还包括,当前只是一个标准的时间戳,如果场景变更,我的代码改成了某个时间戳再减去一个固定值,如果我不写注释,估计神明也难找到这个魔鬼常数的规律,这个接口将以无比反人类行为被人唾弃。
constexpr的存在就可以解决或者缓解这种反人类行为。例如我可以直接定义一个constexpr函数,把一串格式化的时间字符直接装化成这个参数,这样下个开发者读到这里时,很容易就能发现这个参数的来龙去脉,调用也会很银杏化!
这个常量计算最初可以追溯到c语言的define手动代码内联替换,不过如果你同时也是一名c语言开发者,你会发现define的缺点明显。首先是不可调试,如果报错在某个魔法宏里面,神仙也得抓脑壳,让本就不富裕的发量雪上加霜!其次是设计复杂和条件苛刻,包括但不限于不能指定类型,递归设计复杂,不能换行导致需要用很多个 \ 去增强可读性,甚至还会因此无法获得IDE智能感知,代码补全的支持。
宏这玩意在编译器报错前,没人能知道它的小毛病,在报错后除了设计者没人愿意去修它的bug。毕竟人不是编译器,即使是VS这种能辅助展开宏的银杏化工具展开的宏代码我都不想去读,对齐梦魇,人脑模拟演算替换。如果出错了让你去改,就像极了一坨屎放你餐盘里,你还得硬着头皮吃下去。。。
常量表达式的应用还有很多,比如著名的fmt库的格式化解析,比如还某些复杂公式的常量值如定点,某个中间值均可以转化成银杏化便于人眼识别的语句等等,真可谓是给码农续命的一大利器。
穷举出所有结果列出来是目的,不是手段啊。
constexpr consteval 的目的是 :
自动 "在需要使用constexpr模板和元编程的结果的地方直接填已经算好的结果"
你手填填到猴年马月了:
4 个参数类型的组合就有 4X3X2X1 =24种, 函数体里如果有8个if branch, 那就24*8,
如果每个if branch 再套2层每层 3个 ,那就 24 X8X3.
这还每考虑到 那些可以计算不可返回的 编译期类型比如 std::vector 和 constexpr placement new带来的结果阶乘式增加。
constexpr consteval 的目的确实是打表 是 直接填已经算好的结果, 它是实现这些目的的手段。
你的思考角度 有限制: 你只考虑了 最终结果是一个 【简单值】的情况。 这是单点。
实际编译期计算和编译期推导主要解决的是一类2维的问题:

f() {
   if constexpr(A) {
      if constexpr(C) {
      } else if constexpr(D){
      }
   } else if constexpr(B) {
      if constexpr(C) {
      } else if constexpr(D){
      }
   }
}

上面这个的值域 有下列可能

f0() {}
f1() { A{} }
f2() { A{C{}} }
f3() { A{D{}} }
f4() { B{} }
f5() { B{C{}} }
f6() { B{D{}} }

假如变化因素的个数是 n0 n1 n2 n3......,那么枚举的结果数量级是 阶乘的,手写明显是不现实的。

阶乘结果是2维的,穷举就已经很大了。
而加入编译期for-loop 和 while 之后, 是个3维的。编译期的可使用不可返回的可变容器比如std::vector 性质和for-loop类似 也是一个独立维度(当然实现时不可能老实展开,可以利用编译期解释)。 compile-time-placement-new 性质也类似更复杂一些因为分配器有控制逻辑,控制逻辑本身也是一个维度。
实际上还可能是4维的,如果算上 & | &&| const| noexcept 这个specifier维度。
编译期展开的本质确实是算一个笛卡尔积,但是这东西容易爆炸,所以必需有工具(constexpr consteval)辅助(编译过程中一些维度是可以被剪枝略过的),手写是绝对不现实的。
你可以考虑一个简单的2维表
行是状态,列是 可变因素组合,行列交叉是 下一个状态。
整个东西确实是全部可以手填的,但是你想想它的数量级,即使是一个只有50行的小函数,上百个状态还是有的,不靠工具你手写不出的,人要是能手填这个,那计算机根本就没有存在的必要,人脑就算出来了。
[收藏本文] 【下载本文】
   设计艺术 最新文章
如果由你设计更合理的kaiserreich你会怎么做
如何激怒一位美术生?
入坑了,出不来了,有没有人再推荐点你见人
为什么德云社的破事儿听起来都像在听封建社
为什么3D建模blender最好用却很少学blender
今年你拍下的哪些瞬间,给人「春天来了」的
有哪些游戏的最终 BOSS 由于设计的太难,导
“角色也有自己的生活”是什么时候开始成为
为什么All in Ai的百度做不出来sora?
为什么要上学上学的意义是什么?
上一篇文章      下一篇文章      查看所有文章
加:2025-02-26 09:47:39  更:2025-02-26 13:37:18 
 
 
股票涨跌实时统计 涨停板选股 分时图选股 跌停板选股 K线图选股 成交量选股 均线选股 趋势线选股 筹码理论 波浪理论 缠论 MACD指标 KDJ指标 BOLL指标 RSI指标 炒股基础知识 炒股故事
网站联系: qq:121756557 email:121756557@qq.com  天天财汇