【问题标题】:Why can I modify a string defined using defparameter?为什么我可以修改使用 defparameter 定义的字符串?
【发布时间】:2021-12-24 13:31:21
【问题描述】:

我在 SBCL 2.0.1 中试过这个:

(let ((s "Tom's house"))
  (setf (subseq s 0 5) "Cat")
  s)

我收到警告:

; in: LET ((S "Tom's house"))
;     (SETF (SUBSEQ S 0 5) "Cat")
; --> LET* 
; ==>
;   (REPLACE #:SEQUENCE #:NEW1 :START1 0 :END1 5)
; 
; caught WARNING:
;   Destructive function REPLACE called on constant data: "Tom's house"
;   See also:
;     The ANSI Standard, Special Operator QUOTE
;     The ANSI Standard, Section 3.2.2.3
; 
; compilation unit finished
;   caught 1 WARNING condition

但是当我尝试下面的代码时,我没有收到任何警告或错误。为什么我可以修改使用defparameter(或defvar)定义的字符串,但不能修改使用let定义的字符串?

(defparameter *s* "Tom's house")
(setf (subseq *s* 0 3) "Cat")

【问题讨论】:

  • 编译文件时可能会出现这样的警告。似乎没有特定原因不发出警告,只是缺少此功能。
  • @RainerJoswig 在 Common Lisp 中是否完全不允许修改字符串?
  • 这是不允许的。您可以修改在运行时创建的字符串。但是对于文字字符串(嵌入在源代码中的字符串),修改它们的效果在可移植的 Common Lisp 中是未定义的。例如,如果您有两个具有相同内容的字符串的 DEFPARAMETER 表单,那么编译器可能会优化空间并仅分配一个字符串 -> 为两个变量重用此字符串。然后修改一个字符串会看到两个不同变量的效果。另一个影响可能是编译器在只读内存中分配代码(以及它的数据)。
  • 这种只读内存分配是不正常的,但在一些较新的系统上,这实际上可能更常见:ARM 上的 Apple iOS 可能不允许代码修改(这也是为什么 iOS 上的 Common Lisp 编译器不能允许在运行时创建/加载代码),最新的 macOS 也有类似的功能。
  • “如果文字对象(包括引用对象)被破坏性修改,后果是不确定的”在quote的定义中更合适

标签: common-lisp


【解决方案1】:

如 cmets 中所述:不允许修改文字对象,特别是来自 the definition of quote

如果文字对象(包括引用对象)被破坏性修改,后果是不确定的。

这意味着“不要在符合标准的程序中这样做”。它并不意味着是'系统必须阻止你这样做'。

特别应该明确的是,确实阻止你这样做的系统要么必须将所有文字分配到内存的某个特殊区域,以便内存保护可以处理问题,要么有一系列秘密配对的可变/不可变类型的对象,可以是文字(或者可能是对象标签中的“可变”位)。我认为后者是像 Racket 这样的语言所做的:例如,它们具有可变和不可变的字符串。

要求实现来检查这一点需要的策略可能非常困难,其中一些甚至可能并不总是可行(例如,特殊内存区域技巧假设架构支持内存页面上的只读位,这不是语言应该假设的)。所以语言规范只是说“后果是不确定的”。

但是,很明显,在某些情况下,智能编译器可以检测到一些明显伪造的代码。一个是这样的:

(let ((x "literal string"))
  ... do not assign to x ...
  (setf (char x 0) ...)
  ...)

智能编译器(尤其是进行花哨类型推断的编译器)可以很容易地看到您正在变异的 x 的值是一个文字字符串,并且可以在编译时警告您和/或在运行时引发异常-时间。

将其与您的第二个示例进行比较:

(defparameter *x* "a literal string")
...
(setf (char *x* 0) ...)

为了解决这个问题,编译器必须证明 *x* 在您尝试改变其值时实际上 仍然是一个文字字符串。这样做需要某种整体程序分析:它需要知道在*x* 的定义和赋值之间发生的所有事情。虽然这也许有时是可能的——例如,代码在一个正在编译的文件中,你使用defparameter(因为defvar 不起作用!)之间没有任何关系定义和突变——当然这并不总是可能的。

所以您看到的是 SBCL 编译器成功地检测到了某些情况,但不是全部。这很好:这总比没有检测到要好。

【讨论】:

  • “因为defvar 不起作用!” — 为什么defvar 不起作用?
  • @Flux:因为在(defvar *x* "foo") 之后,所有编译器都可以知道*x* 是绑定的:它根本不知道它绑定到的事物的类型。
【解决方案2】:

我已经尝试在 SBCL 的 REPL 中运行上述代码。我确认上述意见。

正如错误消息所暗示的那样,真正的潜在问题来自replace

; doesn't work
(let ((s "Tom's house"))
  (setf (subseq s 0 5) "Cat")
   s)

; does work
(defparameter *s* "Tom's house")
(setf (subseq *s* 0 3) "Cat")

; also works
(replace "Tom's house" "Cat")

所以:

  1. 我们可以更改文字字符串而无需投诉或警告
  2. 问题似乎来自let

探索更多:

(defun myfunc ()
   (let ((s "Tom's house"))
      s))

反汇编为(disassemble 'myfunc)

; disassembly for MYFUNC
; Size: 30 bytes. Origin: #x1003CA1DD3               ;MYFUNC
; D3:       498B4510         MOV RAX, [R13+16]               
; D7:       488945F8         MOV [RBP-8], RAX
; DB:       840425F8FF1020   TEST AL, [#x2010FFF8]   ; 
; E2:       488B15B7FFFFFF   MOV RDX, [RIP-73]       ; "Tom's house"
; E9:       488BE5           MOV RSP, RBP
; EC:       F8               CLC
; ED:       5D               POP RBP
; EE:       C3               RET
; EF:       CC10             INT3 16    ; Invalid argument count trap
NIL

字符串字面量似乎保存在堆上,并被指向,而不是在堆栈上。

我猜let 不喜欢它的值被这样改变。

【讨论】:

  • 警告不是错误
猜你喜欢
  • 1970-01-01
  • 2020-09-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-08-12
  • 1970-01-01
相关资源
最近更新 更多