What's the best way to deal with a non-const-aware Library/SDK?
我已经在3dsmax sdk上工作了更长时间,它几乎在所有部分都不使用const。因此,即使是Bitmap的Width()或Height()吸气剂也没有标记为const。在小项目中,这已经是一个真正的烦恼了,但自从我在一个更大的项目中工作以来,它变得越来越可怕。
例如,出于性能原因,我在多个类实例中持有单个Bitmap实例作为shared_ptr成员。当然,有一些情况我想尽量避免单个实例可能会更改所有实例的属性,因此所有原始指针getter(对于sdk是必需的)都会提供const Bitmap*。不幸的是,现在我甚至不能要求const Bitmap*的宽度,因为Width()是非常量。
我在问自己,解决这个问题的最好方法是什么。我看到三个选项:
- 完全忘记常量,把一切都变成非常量。在小项目中,我曾经这样做过,但正如我所说,使用更复杂的技术,它会变得更危险。
- 在任何必要的地方都要做一个就地的。在很多地方都会这样,而且读起来很糟糕。
- 为3dsmax类编写包装器,这些类至少为可能非常安全的方法提供const方法。这将把所有的const_cast封装在一个地方,也适用于其他项目。
我被警告(我也知道)这可能是基于意见的。但我现在不得不长期处理这个烦人的问题,我真的很想找到一个解决办法,因此需要其他人的经验。
- 第四种选择:抛弃图书馆。从你所说的来看,这并不能激发信心。
- 哈哈,是的,这是一个更好的选择:)但不幸的是,这意味着我也不得不放弃我的一部分工作…
- 我在这里看到了两个解决方案:把每一件东西都用一个易于理解的包装纸(你的第三个选择)包装起来,就像@juanchopanza建议的那样,把图书馆扔掉。
- 我认为第三个选项在现代函数编程方面更好,所有的都是常量,使用变量是例外。
- 包装API时要考虑的一个问题是增加了维护负担。如果他们的API发生了变化,您必须在包装纸中反映出这些变化。
- 我不会想得太多而选择数字1;const-correction有点像git中的干净历史(rebase而不是merge):理论上很好,人们在其中投入了大量时间,但最终却高估了它的有用性。许多其他主流语言没有它,它们做得很好。
- @Galik我强烈反对您的说法,API包装器倾向于减少维护负担,因为这种方法能够将由包装器本身内部的API更改引起的修改封闭起来,通常不需要使用包装器修改代码。也就是说,人们通常可以通过只更改包装器而不是更改整个代码库来摆脱困境。
- @Matteoitalia听起来是个很糟糕的建议。根据我的经验,没有常量的语言缺乏局部性,这给代码的用户带来了太多的负担,无法理解正在发生的事情。在我的经验中,语言w/o const是"好"的,唯一的时候就是事物无论如何都不能发生变化(我在考虑函数式编程,但是一种只具有值语义的假想过程语言也可能是好的)。
- @用户2328447?你已经——痛苦地——发现了常量正确性的病毒式本质。我会将const-correct代理包装器作为防火墙写入const-free库/sdk。
- 出于这个原因,我实际上做了一次lib。从这些方面来说,这是可怕的,但在其他方面,这正是我需要的。
- 感谢大家的回答、评论和见解。我现在决定采用VTT的包装方式。
首先,我想说的是,缺乏const的正确性可以通过实现细节来证明,例如getter函数可以对内部同步原语执行锁,因此总是改变内部状态,不能标记为const:
1 2 3 4 5 6 7
| int Bitmap::Width(void)
{
int width{};
::std::lock_guard<::std::mutex> const lock{m_sync};
width = m_width;
return width;
} |
作为一种解决方案,您可以编写一个专用的PIMPL位图包装器,用适当的const限定符限制直接访问感兴趣的位图实现转发函数:
1 2 3 4 5 6 7 8 9 10 11
| class SharedBitmap
{
private: ::std::shared_ptr<Bitmap> m_p_bitmap;
public: int Width(void) const
{
return m_p_bitmap->Width();
}
// other methods...
}; |
号
请注意,这种方法不同于所讨论的第三个选项,因为它不涉及const_cast。
- 这是个好主意-我得考虑一下。但是关于互斥体:我通常特别声明互斥体mutable,因为在我的理解中,它们实际上不是类的一部分/成员,而是一个特殊的项,用于锁定"真正的"成员以防止线程冲突。因此,必须锁定的getter也可以是const。
- CRTP可以提供比PIMPL更好的代码
- @詹姆斯,如何在这里应用CRTP?另外请注意,op已经在使用shared_ptr,因此将其包装到pimpl类中似乎是一个合乎逻辑的下一步。
- @用户2328447我认为在将非常量函数视为非常量函数之前,绝对值得确保它们确实是const。例如,在调用堆栈深处的某个地方,一些未同步的全局变量可能会被修改。
- 例如,getter函数可以对内部同步原语执行锁,因此总是改变内部状态,不能标记为const:"这是胡说——这就是mutable的用途。
- @Juanchopanza真的。不幸的是,我没有完整的源代码,但至少有很多getter是安全的,因为我可以在它们读取成员的头中进行跟踪。
根据我(10年)的经验,"const"比它更有用。更不用说代码变长了,所以更难阅读。如果你想知道一个库是如何工作的,你无论如何都要阅读手册,而不是标题。如果你想知道你做得对,你可以运行功能测试。甚至还有静态分析工具来检查是否曾经写入变量,而不必使用无用的非功能元数据来捕获未记录的使用模式来增加代码的负担。因为有很多方法可以打破常量,所以这是捕获此类错误的正确方法。
总之,根据我的经验,方案1是最有效的解决方案。(这是一种意见吗?不同意的人可能会这么认为。)
对于const的快速后清除,您可以执行#define const甚至-Dconst来删除它,尽管它是否安全可能取决于您的具体情况,但对标准头文件执行此操作是非法的。我也做过类似的黑客,比如#define private|protected public,而不是在做白盒测试的时候干扰friend,工作起来很有魅力!
知道在许多编程语言中,"常量变量"的概念是无效的,如果没有它,它们似乎做得很好。
唯一需要常量的时间是C字符串常量/字符串文本。但似乎不是你的情况。
- const为您提供了一个很好的指示函数线程安全性的指标。
- @杰姆斯EDCOX1(2)不给出任何关于函数的线程安全性的提示,因为C++只提供较弱类型的const正确性。也就是说,const-qualified函数仍然可以访问对象的内部状态,或者访问一些可以被其他代码更改的静态值。
- 您是否意识到重新定义C++关键字可以很容易地引入未定义的行为?如果在代码中使用标准库,则所有赌注都将取消。
- @VTT访问状态是只读的,是线程安全的,更改它应该是原子的或互斥保护的,两者都应该是可变的,因此函数可以是常量。至于静态值…最好不要这样做,或者正确地做。当然,要比编译器更聪明是很容易的,但是如果你这样做,你会得到你应得的。
- @詹姆斯没有必要比任何东西都聪明。常量限定符的存在与函数的线程安全性之间没有任何联系。基于常量限定符的存在,对"原子或互斥保护"的内部状态的变化做出任何假设都是完全错误的。
- @Juanchopanza在任意关键字的情况下,是的。在我提到的情况下(const private protected),事实并非如此。在停止const的情况下可能会遇到重复的定义,但这只是不会编译;而不是ub。如果你有什么特别的想法,请告诉我,我会删除任何错误的解决建议。
- @安德烈亚斯,这是标准中规定的。根据哪个关键字没有区别。
- @ JuangopangZa:"你是否意识到重新定义C++关键词可以很容易地引入未定义的行为",这样你就不需要做其他的事情了。当你重新定义一个C++关键字时,你的程序就有了不确定的行为。没有什么可介绍的。
- @安德烈亚斯:"在我提到的情况下(const private protected),不是真的"是真的。
- @lightnessracesinorbit #define const在预处理期间从文件中删除任何出现的令牌const。在此阶段,const没有意义。之后在编译期间没有出现const。如果在预处理、编译或链接期间令牌都不存在,那么它怎么可能是ub?因为它从来没有写过。出示证据,否则就别胡说八道了。
- @安德烈亚斯:我想你不明白乌布是什么。标准规定,如果你这样做,你的程序的行为是未定义的。期间。故事结束。在告诉人们"别胡扯"之前先做基础研究
- 哇,说到废话,我到现在才看你的答案。到处都是可怕的建议。当然,你有权发表自己的意见;但是,你的意见是错误的。:)
- @也许你应该自己做些研究。在您的链接中,有来自标准的引用(即"证明"),表示它仅对标准库是非法的。说说你自己的脚。
- @Andreas如果您使用标准库(直接或间接),那么您就有了ub。顺便说一句,这就是为什么我最初用黄鼠狼的话,"可以很容易地引入未定义的行为",而不是更强大的东西。
- @安德烈亚斯:不,这在您自己的代码中是非法的(除非您不使用标准头,但我希望看到这一点)。切记个人攻击,倾听/学习。