7

假设我正在使用一些具有函数的 C 库:

int foo(char* str);

而且我知道一个事实,即foo()不会修改 . 指向的内存str。它只是写得不好,并没有费心声明str它是不变的。

现在,在我的 C++ 代码中,我目前有:

extern "C" int foo(char* str);

我像这样使用它:

foo(const_cast<char*>("Hello world"));

我的问题:原则上,从语言律师的角度来看,在实践中,我这样写是否安全:

extern "C" int foo(const char* str);

并跳过const_cast'ing?

如果不安全,请说明原因。

注意:我对 C++98 代码的情况特别感兴趣(是的,不幸的是我),所以如果你假设语言标准的更高版本,请说出来。

4

2 回答 2

2

我这样写安全吗:跳过 const_cast'ing?

不。

如果不安全,请说明原因。

-- 从语言方面:

在阅读了dcl.link之后,我认为 C 和 C++ 之间的互操作性究竟是如何工作的并没有完全指定,有许多“不需要诊断”的情况。最重要的部分是:

出现在不同名称空间范围内的具有相同函数名称(忽略限定它的名称空间名称)的具有 C 语言链接的函数的两个声明指的是同一个函数。

因为它们引用相同的函数,所以我相信一个合理的假设是,在 C++ 端声明具有 C 语言链接的标识符必须与在 C 端声明该符号兼容。在 C++ 中没有“兼容类型”的概念,在 C++ 中,两个声明必须相同(在转换之后),这使得限制实际上更加严格。

从 C++ 方面,我们阅读c++draft basic#link-11

在所有类型调整之后(在此期间 typedef 被其定义替换),引用给定变量或函数的所有声明指定的类型应相同,[...]

因为int foo(const char *str)在 C++ 翻译单元中具有 C 语言链接的声明与在 C 翻译单元声明的声明不同int foo(char *str)(因此它具有 C 语言链接),所以行为是未定义的(著名的“无需诊断”)。

从 C 方面(我认为这甚至不需要 - C++ 方面足以使程序具有未定义的行为。无论如何),最重要的部分是C99 6.7.5.3p15

对于要兼容的两种函数类型,两者都应指定兼容的返回类型。此外,参数类型列表(如果两者都存在)应在参数数量和省略号终止符的使用方面达成一致;相应的参数应具有兼容的类型 [...]

因为从C99 6.7.5.1p2 开始

对于要兼容的两种指针类型,两者都应具有相同的限定,并且都应是指向兼容类型的指针。

C99 6.7.3p9

对于要兼容的两种合格类型,两者都应具有兼容类型的相同合格版本 [...]

所以因为char不兼容const char,所以const char *不兼容char *,所以int foo(const char *)不兼容int foo(char*)。调用这样的函数(C99 6.5.2.2p9)将是未定义的行为(您可能还会看到C99 J.2

——从实际方面:

我不相信能够找到一个编译器+架构组合,其中一个翻译单元看到int foo(const char *)而另一个翻译单元定义了一个函数int foo(char *) { /* some stuff */ }并且它“不起作用”。

从理论上讲,一个疯狂的实现可能使用不同的寄存器来传递const char*参数和不同的寄存器来传递char*参数,我希望在那个疯狂的架构 ABI 和编译器中能很好地记录这一点。如果是这样,错误的寄存器将用于参数,它将“不起作用”。

尽管如此,使用简单的包装器仍然没有任何成本:

static inline int foo2(const char *var) {
    return foo(static_cast<char*>(var));
}
于 2020-11-10T10:53:26.337 回答
1

我认为基本答案是:

const 是的,即使引用的对象本身就是const示例中的字符串文字,您也可以放弃。未定义的行为仅指定在尝试修改const对象而不是强制转换的结果时出现。这些规则及其存在的理由是“旧的”。我确信它们早于 C++98。

与之相比,任何通过非易失性引用访问volatile易失性对象的尝试都是未定义的行为。我只能在这里读取“访问”作为读取和/或写入。

我不会重复其他建议,但这是最偏执的解决方案。这不是因为 C++ 语义不清楚,而是偏执。他们很清楚。至少如果您接受某些未定义的行为是明确的!

但是你把它描述为“写得不好”,你想在它周围放一些沙袋!

偏执的解决方案依赖于这样一个事实,即如果您传递一个常量对象,它将在整个执行过程中保持不变(如果程序不冒 UB 风险)。

因此,在调用堆栈中制作“hello world”的单个副本,甚至初始化为文件范围对象。您可以在函数中声明它static,并且它只会(以最小的开销)构造一次。

这恢复了字符串文字的几乎所有好处。调用堆栈越低,包括文件范围(全局你说得越好。我不知道传递给的指向对象的生命周期需要多长时间foo()。所以它至少需要足够低链来满足该条件。注意:C++ 98 有std::string,但在这里不太行,因为您仍然被禁止修改 . 的结果c_str()。这里定义了语义。

#include <cstring>
#include <iostream>

class pseudo_const{
public:
    pseudo_const(const char*const cstr): str(NULL){
        const size_t sz=strlen(cstr)+1;
        str=new char[sz];
        memcpy(str,cstr,sz);
    }
    

    //Returns a pointer to a life-time permanent copy of 
    //the string passed to the constructor.
    //Modifying the string through this value will be reflected in all
    // subsequent calls.  
    char* get_constlike() const {
        return str;
    }
    
    ~pseudo_const(){
        delete [] str;
    }
private:
    char* str;

};

const pseudo_const str("hello world");

int main() {
    std::cout << str.get_constlike() << std::endl;
    return 0;
}
于 2020-11-10T11:44:03.330 回答