Truyền không ký để ký hiệu quả tránh hành vi do triển khai xác định


94

Tôi muốn xác định một hàm nhận một unsigned intđối số làm đối số và trả về một intmô-đun đồng dư UINT_MAX + 1 cho đối số.

Lần thử đầu tiên có thể trông như thế này:

int unsigned_to_signed(unsigned n)
{
    return static_cast<int>(n);
}

Nhưng như bất kỳ luật sư ngôn ngữ nào cũng biết, việc truyền từ chưa ký sang có ký cho các giá trị lớn hơn INT_MAX được xác định bởi việc triển khai.

Tôi muốn thực hiện điều này sao cho (a) nó chỉ dựa trên hành vi được yêu cầu bởi spec; và (b) nó biên dịch thành no-op trên bất kỳ máy hiện đại nào và tối ưu hóa trình biên dịch.

Đối với các máy kỳ lạ ... Nếu không có int congruent modulo UINT_MAX + 1 có dấu cho int chưa được ký, giả sử tôi muốn đưa ra một ngoại lệ. Nếu có nhiều hơn một (tôi không chắc điều này có thể xảy ra), giả sử tôi muốn có cái lớn nhất.

OK, lần thử thứ hai:

int unsigned_to_signed(unsigned n)
{
    int int_n = static_cast<int>(n);

    if (n == static_cast<unsigned>(int_n))
        return int_n;

    // else do something long and complicated
}

Tôi không quan tâm lắm đến hiệu quả khi tôi không sử dụng hệ thống bổ sung hai mặt điển hình, vì theo ý kiến ​​khiêm tốn của tôi thì điều đó khó xảy ra. Và nếu mã của tôi trở thành một nút thắt cổ chai trên các hệ thống cường độ ký hiệu ở khắp nơi của năm 2050, thì tôi cá rằng ai đó có thể tìm ra điều đó và tối ưu hóa nó sau đó.

Bây giờ, nỗ lực thứ hai này đã khá gần với những gì tôi muốn. Mặc dù quá trình ép kiểu tới intđược xác định thực thi đối với một số đầu vào, việc ép kiểu trở lại unsignedđược đảm bảo theo tiêu chuẩn để bảo toàn mô-đun giá trị UINT_MAX + 1. Vì vậy, điều kiện không kiểm tra chính xác những gì tôi muốn và nó sẽ biên dịch thành không có gì trên bất kỳ hệ thống nào mà tôi có thể gặp phải.

Tuy nhiên ... tôi vẫn đang truyền tới intmà không cần kiểm tra trước xem nó có gọi hành vi do triển khai xác định hay không. Trên một hệ thống giả thuyết nào đó vào năm 2050, nó có thể làm ai-biết-gì. Vì vậy, hãy nói rằng tôi muốn tránh điều đó.

Câu hỏi: "Nỗ lực thứ ba" của tôi trông như thế nào?

Tóm lại, tôi muốn:

  • Truyền từ int chưa ký sang int đã ký
  • Giữ nguyên giá trị mod UINT_MAX + 1
  • Chỉ gọi hành vi được ủy quyền tiêu chuẩn
  • Biên dịch thành no-op trên một máy bổ sung hai chức năng điển hình với trình biên dịch tối ưu hóa

[Cập nhật]

Hãy để tôi đưa ra một ví dụ cho thấy tại sao đây không phải là một câu hỏi tầm thường.

Hãy xem xét một triển khai C ++ giả định với các thuộc tính sau:

  • sizeof(int) bằng 4
  • sizeof(unsigned) bằng 4
  • INT_MAX bằng 32767
  • INT_MINbằng -2 32 + 32768
  • UINT_MAXbằng 2 32 - 1
  • Arithmetic on intlà modulo 2 32 (trong phạm vi INT_MINthông qua INT_MAX)
  • std::numeric_limits<int>::is_modulo là đúng
  • Truyền không dấu nđến int sẽ giữ nguyên giá trị cho 0 <= n <= 32767 và nếu không sẽ cho kết quả bằng 0

Trong quá trình triển khai giả định này, có chính xác một intgiá trị đồng dư (mod UINT_MAX + 1) cho mỗi unsignedgiá trị. Vì vậy, câu hỏi của tôi sẽ được xác định rõ.

Tôi khẳng định rằng việc triển khai C ++ giả định này hoàn toàn tuân thủ các đặc tả C ++ 98, C ++ 03 và C ++ 11. Tôi thừa nhận rằng tôi đã không ghi nhớ từng từ của tất cả chúng ... Nhưng tôi tin rằng tôi đã đọc kỹ các phần liên quan. Vì vậy, nếu bạn muốn tôi chấp nhận câu trả lời của bạn, bạn phải (a) trích dẫn một thông số kỹ thuật quy định việc triển khai giả định này hoặc (b) xử lý nó một cách chính xác.

Thật vậy, một câu trả lời đúng phải xử lý mọi việc triển khai giả định được tiêu chuẩn cho phép. Theo định nghĩa, "chỉ gọi hành vi bắt buộc theo tiêu chuẩn" nghĩa là gì.

Ngẫu nhiên, lưu ý rằng std::numeric_limits<int>::is_modulohoàn toàn vô dụng ở đây vì nhiều lý do. Đối với một điều, nó có thể xảy ra truengay cả khi các phôi chưa được ký để ký không hoạt động đối với các giá trị chưa được ký lớn. Đối với người khác, nó truethậm chí có thể nằm trên hệ thống phần bù hoặc dấu hiệu của một người, nếu số học chỉ đơn giản là mô đun của toàn bộ phạm vi số nguyên. Và như thế. Nếu câu trả lời của bạn phụ thuộc vào is_modulo, nó sai.

[Cập nhật 2]

Câu trả lời của hvd đã dạy tôi điều gì đó: Việc triển khai C ++ giả định của tôi cho các số nguyên không được phép bởi C. Các tiêu chuẩn C99 và C11 rất cụ thể về việc biểu diễn các số nguyên có dấu; thực sự, chúng chỉ cho phép hai phần bù, phần bổ sung một và độ lớn dấu hiệu (mục 6.2.6.2 đoạn (2);).

Nhưng C ++ không phải là C. Hóa ra, sự thật này nằm ở trọng tâm của câu hỏi của tôi.

Chuẩn C ++ 98 ban đầu dựa trên C89 cũ hơn nhiều, cho biết (phần 3.1.2.5):

Đối với mỗi kiểu số nguyên có dấu, có một kiểu số nguyên không dấu tương ứng (nhưng khác nhau) (được chỉ định bằng từ khóa không dấu) sử dụng cùng một lượng lưu trữ (bao gồm thông tin dấu) và có cùng yêu cầu về căn chỉnh. Phạm vi các giá trị không âm của kiểu số nguyên có dấu là một dải con của kiểu số nguyên không dấu tương ứng và cách biểu diễn cùng một giá trị trong mỗi kiểu là như nhau.

C89 không nói gì về việc chỉ có một bit dấu hoặc chỉ cho phép hai phần bổ sung / một phần bổ sung / dấu hiệu-độ lớn.

Tiêu chuẩn C ++ 98 đã áp dụng ngôn ngữ này gần như nguyên văn (phần 3.9.1 đoạn (3)):

Đối với mỗi kiểu số nguyên có dấu, tồn tại một kiểu số nguyên không dấu tương ứng (nhưng khác nhau) : " unsigned char", " unsigned short int", " unsigned int" và " unsigned long int", mỗi kiểu chiếm cùng một lượng bộ nhớ và có cùng yêu cầu căn chỉnh (3.9 ) là kiểu số nguyên có dấu tương ứng; nghĩa là mỗi kiểu số nguyên có dấu có cùng một biểu diễn đối tượng như kiểu số nguyên không dấu tương ứng của nó . Dải các giá trị không âm của kiểu số nguyên có dấu là một dải con của kiểu số nguyên không dấu tương ứng và cách biểu diễn giá trị của mỗi kiểu có dấu / không dấu tương ứng sẽ giống nhau.

Chuẩn C ++ 03 sử dụng ngôn ngữ cơ bản giống hệt nhau, cũng như C ++ 11.

Không có thông số C ++ chuẩn nào hạn chế các biểu diễn số nguyên có dấu của nó với bất kỳ thông số C nào, theo như tôi có thể nói. Và không có gì bắt buộc một bit ký hiệu hay bất cứ thứ gì thuộc loại này. Tất cả những gì nó nói là các số nguyên có dấu không âm phải là một dãy con của các số nguyên không dấu tương ứng.

Vì vậy, một lần nữa tôi khẳng định rằng INT_MAX = 32767 với INT_MIN = -2 32 +32768 được cho phép. Nếu câu trả lời của bạn giả định khác, nó không chính xác trừ khi bạn trích dẫn một tiêu chuẩn C ++ chứng minh tôi sai.


@SteveJessop: Trên thực tế, tôi đã nêu chính xác những gì tôi muốn trong trường hợp đó: "Nếu không có int congruent modulo UINT_MAX + 1 có dấu cho int không dấu, giả sử tôi muốn đưa ra một ngoại lệ." Đó là, tôi muốn int được ký "đúng" miễn là nó tồn tại. Nếu nó không tồn tại - như có thể xảy ra trong trường hợp ví dụ như các bit đệm hoặc các biểu diễn bổ sung đơn - tôi muốn phát hiện điều đó và xử lý nó cho lệnh gọi cụ thể đó của diễn viên.
Nemo

xin lỗi, không chắc làm thế nào tôi bỏ lỡ điều đó.
Steve Jessop

Btw, tôi nghĩ rằng trong triển khai phức tạp giả định của bạn intcần ít nhất 33 bit để đại diện cho nó. Tôi biết đó chỉ là một chú thích cuối trang, vì vậy bạn có thể tranh luận rằng nó không chuẩn mực, nhưng tôi nghĩ chú thích 49 trong C ++ 11 là đúng (vì đó là định nghĩa của một thuật ngữ được sử dụng trong tiêu chuẩn) và nó không mâu thuẫn bất cứ điều gì được nêu rõ ràng trong văn bản quy phạm. Vì vậy, tất cả các giá trị âm phải được biểu diễn bằng một mẫu bit trong đó bit cao nhất được đặt, và do đó bạn không thể nhồi nhét 2^32 - 32768chúng thành 32 bit. Không phải lập luận của bạn dựa theo bất kỳ cách nào vào kích thước của int.
Steve Jessop

Và liên quan đến các chỉnh sửa của bạn trong câu trả lời của hvd, tôi nghĩ bạn đã giải thích sai ghi chú 49. Bạn nói rằng độ lớn ký hiệu bị cấm, nhưng không phải vậy. Bạn đã đọc nó như sau: "các giá trị được biểu thị bởi các bit liên tiếp là phép cộng, bắt đầu bằng 1 và (được nhân với lũy thừa liên tiếp của 2, có lẽ ngoại trừ bit có vị trí cao nhất)". Tôi tin rằng nó nên được đọc, "các giá trị được biểu thị bằng các bit liên tiếp (là phép cộng, bắt đầu bằng 1 và được nhân với lũy thừa tích phân liên tiếp của 2), có lẽ ngoại trừ bit có vị trí cao nhất". Đó là, tất cả các cược sẽ tắt nếu bit cao được đặt.
Steve Jessop

@SteveJessop: Diễn giải của bạn có thể đúng. Nếu vậy, nó không loại trừ giả thuyết của tôi ... Nhưng nó cũng đưa ra một số lượng lớn các khả năng thực sự, khiến câu hỏi này trở nên cực kỳ khó trả lời. Điều này thực sự trông giống như một lỗi trong thông số kỹ thuật đối với tôi. (Rõ ràng, ủy ban C đã nghĩ như vậy và sửa nó kỹ lưỡng trong C99. Tôi tự hỏi tại sao C ++ 11 không áp dụng cách tiếp cận của họ?)
Nemo

Câu trả lời:


70

Mở rộng câu trả lời của người dùng71404:

int f(unsigned x)
{
    if (x <= INT_MAX)
        return static_cast<int>(x);

    if (x >= INT_MIN)
        return static_cast<int>(x - INT_MIN) + INT_MIN;

    throw x; // Or whatever else you like
}

Nếu x >= INT_MIN(ghi nhớ các quy tắc khuyến mại, INT_MINđược chuyển đổi thành unsigned), thì x - INT_MIN <= INT_MAXđiều này sẽ không có bất kỳ sự cố tràn nào.

Nếu điều đó không rõ ràng, hãy xem xác nhận quyền sở hữu "Nếu x >= -4uthì x + 4 <= 3." Và lưu ý rằng điều đó INT_MAXsẽ bằng ít nhất giá trị toán học của -INT_MIN - 1.

Trên các hệ thống phổ biến nhất, khi !(x <= INT_MAX)ngụ ý x >= INT_MIN, trình tối ưu hóa sẽ có thể (và trên hệ thống của tôi, có thể) loại bỏ kiểm tra thứ hai, xác định rằng hai returncâu lệnh có thể được biên dịch thành cùng một mã và cũng xóa kiểm tra đầu tiên. Danh sách lắp ráp đã tạo:

__Z1fj:
LFB6:
    .cfi_startproc
    movl    4(%esp), %eax
    ret
    .cfi_endproc

Việc triển khai giả định trong câu hỏi của bạn:

  • INT_MAX bằng 32767
  • INT_MIN bằng -2 32 + 32768

là không thể, vì vậy không cần xem xét đặc biệt. INT_MINsẽ bằng một trong hai -INT_MAX, hoặc -INT_MAX - 1. Điều này xuất phát từ cách biểu diễn kiểu số nguyên (6.2.6.2) của C, yêu cầu ncác bit là bit giá trị, một bit là bit dấu và chỉ cho phép một biểu diễn bẫy duy nhất (không bao gồm các biểu diễn không hợp lệ do có các bit đệm), cụ thể là cái mà sẽ đại diện cho số không âm / -INT_MAX - 1. C ++ không cho phép bất kỳ biểu diễn số nguyên nào ngoài những gì C cho phép.

Cập nhật : Trình biên dịch của Microsoft dường như không nhận thấy điều đóx > 10x >= 11kiểm tra điều tương tự. Nó chỉ tạo ra mã mong muốn nếux >= INT_MINđược thay thế bằng mãx > INT_MIN - 1umà nó có thể phát hiện là phủ định củax <= INT_MAX(trên nền tảng này).

[Cập nhật từ người hỏi (Nemo), trình bày chi tiết về cuộc thảo luận của chúng tôi bên dưới]

Bây giờ tôi tin rằng câu trả lời này hoạt động trong mọi trường hợp, nhưng vì những lý do phức tạp. Tôi có khả năng trao tiền thưởng cho giải pháp này, nhưng tôi muốn nắm bắt tất cả các chi tiết đẫm máu trong trường hợp có ai đó quan tâm.

Hãy bắt đầu với C ++ 11, phần 18.3.3:

Bảng 31 mô tả tiêu đề <climits>.

...

Nội dung giống như tiêu đề thư viện C Chuẩn <limits.h>.

Ở đây, "Chuẩn C" có nghĩa là C99, đặc điểm kỹ thuật của nó hạn chế nghiêm trọng việc biểu diễn các số nguyên có dấu. Chúng giống như số nguyên không dấu, nhưng với một bit dành riêng cho "dấu" và không hoặc nhiều bit dành riêng cho "padding". Các bit đệm không đóng góp vào giá trị của số nguyên và bit dấu chỉ đóng góp như hai phần bù, phần bù một hoặc độ lớn của dấu.

Vì C ++ 11 kế thừa các <climits>macro từ C99, INT_MIN là -INT_MAX hoặc -INT_MAX-1 và mã của hvd được đảm bảo hoạt động. (Lưu ý rằng, do khoảng đệm, INT_MAX có thể nhỏ hơn nhiều so với UINT_MAX / 2 ... Nhưng nhờ vào cách thức hoạt động của phôi có dấu-> không dấu, câu trả lời này xử lý tốt.)

C ++ 03 / C ++ 98 phức tạp hơn. Nó sử dụng cùng một từ ngữ kế thừa <climits>từ "Tiêu chuẩn C", nhưng bây giờ "Tiêu chuẩn C" có nghĩa là C89 / C90.

Tất cả những điều này - C ++ 98, C ++ 03, C89 / C90 - có từ ngữ tôi đưa ra trong câu hỏi của mình, nhưng cũng bao gồm điều này (C ++ 03 phần 3.9.1 đoạn 7):

Việc biểu diễn các dạng tích phân phải xác định các giá trị bằng cách sử dụng hệ thống số nhị phân thuần túy. (44) [ Ví dụ : tiêu chuẩn này cho phép phần bù của 2, phần bù của 1 và các biểu diễn cường độ có dấu cho các dạng tích phân.]

Chú thích cuối trang (44) định nghĩa "hệ thống số nhị phân thuần túy":

Biểu diễn vị trí cho số nguyên sử dụng các chữ số nhị phân 0 và 1, trong đó các giá trị được biểu thị bằng các bit liên tiếp là phép cộng, bắt đầu bằng 1 và được nhân với lũy thừa liên tiếp của 2, ngoại trừ bit có vị trí cao nhất.

Điều thú vị về cách diễn đạt này là nó mâu thuẫn với chính nó, bởi vì định nghĩa của "hệ thống số nhị phân thuần túy" không cho phép biểu diễn dấu / độ lớn! Nó cho phép bit cao có giá trị -2 n-1 (phần bù hai phần) hoặc - (2 n-1 -1) (phần bù một). Nhưng không có giá trị nào cho bit cao dẫn đến dấu / độ lớn.

Dù sao, "triển khai giả định" của tôi không đủ điều kiện là "nhị phân thuần túy" theo định nghĩa này, vì vậy nó bị loại trừ.

Tuy nhiên, thực tế là bit cao là đặc biệt có nghĩa là chúng ta có thể tưởng tượng nó đóng góp bất kỳ giá trị nào: Một giá trị dương nhỏ, giá trị dương lớn, giá trị âm nhỏ hoặc giá trị âm rất lớn. (Nếu bit dấu có thể đóng góp - (2 n-1 -1), tại sao không - (2 n-1 -2)? V.v.)

Vì vậy, hãy tưởng tượng một biểu diễn số nguyên có dấu chỉ định một giá trị sai cho bit "dấu".

Một giá trị dương nhỏ cho bit dấu hiệu sẽ dẫn đến một phạm vi dương cho int(có thể lớn bằng unsigned), và mã của hvd xử lý tốt.

Một giá trị dương lớn cho bit dấu sẽ dẫn đến intviệc có giá trị lớn hơn tối đa unsigned, điều này bị cấm.

Một giá trị âm rất lớn cho bit dấu hiệu sẽ dẫn đến việc intđại diện cho một dải giá trị không liền nhau và các từ khác trong các quy tắc đặc tả.

Cuối cùng, làm thế nào về một bit dấu hiệu đóng góp một số lượng âm nhỏ? Chúng ta có thể có 1 trong "bit dấu" đóng góp, chẳng hạn, -37 vào giá trị của int? Vì vậy, khi đó INT_MAX sẽ là (giả sử) 2 31 -1 và INT_MIN sẽ là -37?

Điều này sẽ dẫn đến một số số có hai biểu diễn ... Nhưng phần bù một cho hai biểu diễn bằng 0 và điều đó được cho phép theo "Ví dụ". Không nơi nào thông số kỹ thuật nói rằng số không là số nguyên duy nhất có thể có hai biểu diễn. Vì vậy, tôi nghĩ rằng giả thuyết mới này được cho phép bởi thông số kỹ thuật.

Thật vậy, bất kỳ giá trị âm nào từ -1 trở xuống -INT_MAX-1dường như đều được phép dùng làm giá trị cho "bit dấu", nhưng không có giá trị nào nhỏ hơn (e rằng phạm vi không liền nhau). Nói cách khác, INT_MINcó thể là bất kỳ giá trị nào từ -INT_MAX-1-1.

Bây giờ, hãy đoán xem? Đối với lần ép kiểu thứ hai trong mã của hvd để tránh hành vi do triển khai xác định, chúng ta chỉ cần x - (unsigned)INT_MINnhỏ hơn hoặc bằng INT_MAX. Chúng tôi chỉ cho thấy INT_MINlà ít nhất -INT_MAX-1. Rõ ràng, xlà nhiều nhất UINT_MAX. Truyền một số âm sang không dấu cũng giống như thêm UINT_MAX+1. Đặt nó tất cả cùng nhau:

x - (unsigned)INT_MIN <= INT_MAX

nếu và chỉ nếu

UINT_MAX - (INT_MIN + UINT_MAX + 1) <= INT_MAX
-INT_MIN-1 <= INT_MAX
-INT_MIN <= INT_MAX+1
INT_MIN >= -INT_MAX-1

Cuối cùng đó là những gì chúng tôi vừa chỉ ra, vì vậy ngay cả trong trường hợp sai lầm này, mã vẫn thực sự hoạt động.

Điều đó làm cạn kiệt tất cả các khả năng, do đó kết thúc bài tập cực kỳ hàn lâm này.

Điểm mấu chốt: Có một số hành vi chưa được chỉ định nghiêm trọng đối với các số nguyên có dấu trong C89 / C90 đã được kế thừa bởi C ++ 98 / C ++ 03. Nó được sửa trong C99 và C ++ 11 gián tiếp kế thừa bản sửa lỗi bằng cách kết hợp <limits.h>từ C99. Nhưng ngay cả C ++ 11 vẫn giữ nguyên từ ngữ "biểu diễn nhị phân thuần túy" tự mâu thuẫn ...


Đã cập nhật câu hỏi. Tôi bỏ phiếu xuống câu trả lời này (hiện tại) để làm nản lòng những người khác ... Tôi sẽ bỏ phiếu xuống sau vì câu trả lời rất thú vị. (Đúng cho C, nhưng sai cho C ++. Tôi nghĩ vậy.)
Nemo

@Nemo Tiêu chuẩn C áp dụng cho C ++ trong trường hợp này; ít nhất, các giá trị trong <limits.h>được định nghĩa trong tiêu chuẩn C ++ có cùng ý nghĩa như trong tiêu chuẩn C, vì vậy tất cả các yêu cầu của C đối với INT_MININT_MAXđược kế thừa trong C ++. Bạn đúng khi C ++ 03 đề cập đến C90 và C90 mơ hồ về các biểu diễn số nguyên được phép, nhưng sự thay đổi C99 (ít nhất được kế thừa <limits.h>bởi C ++ 11, hy vọng cũng theo một cách đơn giản hơn) để giới hạn nó ở ba cái đó là một cái đã hệ thống hóa thực tiễn hiện có: không có triển khai nào khác tồn tại.

Tôi đồng ý rằng ý nghĩa của INT_MINvv được kế thừa từ C. Nhưng điều đó không có nghĩa là các giá trị được. (Thật vậy, làm sao chúng có thể, vì mọi cách triển khai đều khác nhau?) Suy luận của bạn INT_MINnằm trong khoảng 1 -INT_MAXphụ thuộc vào từ ngữ đơn giản không xuất hiện trong bất kỳ thông số C ++ nào. Vì vậy, mặc dù C ++ kế thừa ý nghĩa ngữ nghĩa của các macro, nhưng spec không cung cấp (hoặc kế thừa) từ ngữ hỗ trợ suy luận của bạn. Điều này dường như là một sự giám sát trong thông số kỹ thuật C ++ ngăn cản quá trình truyền không ký để ký hiệu quả hoàn toàn phù hợp.
Nemo

@Nemo Nếu bạn (có lẽ chính xác) tuyên bố rằng C ++ cho phép các biểu diễn khác, thì trên cách triển khai như vậy, tôi khẳng định rằng đó INT_MIN không bắt buộc phải là giá trị có thể biểu diễn tối thiểu của kiểu int, bởi vì theo như C liên quan, nếu kiểu không phù hợp với các yêu cầu của int, tiêu chuẩn C không thể bao hàm việc triển khai đó theo bất kỳ cách nào và tiêu chuẩn C ++ không đưa ra bất kỳ định nghĩa nào về nó ngoài "tiêu chuẩn C nói gì". Tôi sẽ kiểm tra xem có lời giải thích đơn giản hơn không.

7
Điều này là tuyệt đẹp. Không hiểu sao tôi lại bỏ lỡ câu hỏi này vào thời điểm đó.
Lightness Races ở Orbit

17

Mã này chỉ dựa trên hành vi, được yêu cầu bởi đặc tả, vì vậy yêu cầu (a) dễ dàng được đáp ứng:

int unsigned_to_signed(unsigned n)
{
  int result = INT_MAX;

  if (n > INT_MAX && n < INT_MIN)
    throw runtime_error("no signed int for this number");

  for (unsigned i = INT_MAX; i != n; --i)
    --result;

  return result;
}

Nó không dễ dàng như vậy với yêu cầu (b). Điều này biên dịch thành một no-op với gcc 4.6.3 (-Os, -O2, -O3) và với clang 3.0 (-Os, -O, -O2, -O3). Intel 12.1.0 từ chối tối ưu hóa điều này. Và tôi không có thông tin gì về Visual C.


1
OK, điều này thật tuyệt. Tôi ước gì tôi có thể chia tiền thưởng 80:20 ... Tôi nghi ngờ lý do của trình biên dịch là: Nếu vòng lặp không kết thúc, resulttràn; số nguyên tràn là không xác định; do đó vòng lặp kết thúc; do đó i == nkhi chấm dứt; do đó resultbằng n. Tôi vẫn phải thích câu trả lời của hvd hơn (đối với hành vi không bệnh lý trên các trình biên dịch kém thông minh hơn), nhưng điều này xứng đáng được nhiều phiếu bầu hơn.
Nemo

1
Không dấu được định nghĩa là mô đun. Vòng lặp cũng được đảm bảo kết thúc vì nlà một số giá trị không dấu và icuối cùng phải đạt đến mọi giá trị không dấu.
idupree

7

Câu trả lời ban đầu chỉ giải quyết vấn đề cho unsigned=> int. Điều gì xảy ra nếu chúng ta muốn giải quyết vấn đề chung của "một số kiểu không dấu" thành kiểu có dấu tương ứng của nó? Hơn nữa, câu trả lời ban đầu rất xuất sắc trong việc trích dẫn các phần của tiêu chuẩn và phân tích một số trường hợp góc, nhưng nó không thực sự giúp tôi hiểu tại sao nó hoạt động, vì vậy câu trả lời này sẽ cố gắng đưa ra một cơ sở khái niệm mạnh mẽ. Câu trả lời này sẽ cố gắng giải thích "tại sao" và sử dụng các tính năng C ++ hiện đại để cố gắng đơn giản hóa mã.

Câu trả lời C ++ 20

Vấn đề đã được đơn giản hóa đáng kể với P0907: Số nguyên có dấu là phần bổ sung của haitừ ngữ cuối cùng P1236 đã được bình chọn thành tiêu chuẩn C ++ 20. Bây giờ, câu trả lời càng đơn giản càng tốt:

template<std::unsigned_integral T>
constexpr auto cast_to_signed_integer(T const value) {
    return static_cast<std::make_signed_t<T>>(value);
}

Đó là nó. static_castCuối cùng, kiểu diễn viên kiểu A (hoặc kiểu C) cũng được đảm bảo thực hiện những điều bạn cần cho câu hỏi này và điều mà nhiều lập trình viên nghĩ rằng nó luôn làm.

Câu trả lời C ++ 17

Trong C ++ 17, mọi thứ phức tạp hơn nhiều. Chúng ta phải xử lý ba biểu diễn số nguyên có thể có (phần bù của hai, phần bù của một và độ lớn của dấu). Ngay cả trong trường hợp chúng ta biết nó phải là phần bù của hai vì chúng ta đã kiểm tra phạm vi giá trị có thể có, việc chuyển đổi một giá trị bên ngoài phạm vi của số nguyên có dấu thành số nguyên có dấu đó vẫn cho chúng ta kết quả do triển khai xác định. Chúng ta phải sử dụng các thủ thuật như chúng ta đã thấy trong các câu trả lời khác.

Đầu tiên, đây là mã cho cách giải quyết vấn đề một cách chung chung:

template<typename T, typename = std::enable_if_t<std::is_unsigned_v<T>>>
constexpr auto cast_to_signed_integer(T const value) {
    using result = std::make_signed_t<T>;
    using result_limits = std::numeric_limits<result>;
    if constexpr (result_limits::min() + 1 != -result_limits::max()) {
        if (value == static_cast<T>(result_limits::max()) + 1) {
            throw std::runtime_error("Cannot convert the maximum possible unsigned to a signed value on this system");
        }
    }
    if (value <= result_limits::max()) {
        return static_cast<result>(value);
    } else {
        using promoted_unsigned = std::conditional_t<sizeof(T) <= sizeof(unsigned), unsigned, T>;
        using promoted_signed = std::make_signed_t<promoted_unsigned>;
        constexpr auto shift_by_window = [](auto x) {
            // static_cast to avoid conversion warning
            return x - static_cast<decltype(x)>(result_limits::max()) - 1;
        };
        return static_cast<result>(
            shift_by_window( // shift values from common range to negative range
                static_cast<promoted_signed>(
                    shift_by_window( // shift large values into common range
                        static_cast<promoted_unsigned>(value) // cast to avoid promotion to int
                    )
                )
            )
        );
    }
}

Điều này có nhiều hơn một vài lần so với câu trả lời được chấp nhận và điều đó là để đảm bảo không có cảnh báo không khớp có dấu / không dấu từ trình biên dịch của bạn và để xử lý đúng các quy tắc thăng hạng số nguyên.

Trước tiên, chúng ta có một trường hợp đặc biệt cho các hệ thống không phải là phần bù của hai (và do đó chúng ta phải xử lý giá trị lớn nhất có thể một cách đặc biệt vì nó không có bất kỳ thứ gì để ánh xạ tới). Sau đó, chúng ta đến với thuật toán thực.

Điều kiện cấp cao nhất thứ hai rất đơn giản: chúng ta biết giá trị nhỏ hơn hoặc bằng giá trị lớn nhất, vì vậy nó phù hợp với kiểu kết quả. Điều kiện thứ ba phức tạp hơn một chút ngay cả với các nhận xét, vì vậy một số ví dụ có thể giúp hiểu tại sao mỗi câu lệnh là cần thiết.

Cơ sở khái niệm: dãy số

Đầu tiên, windowkhái niệm này là gì? Hãy xem xét dãy số sau:

   |   signed   |
<.........................>
          |  unsigned  |

Hóa ra là đối với các số nguyên bù của hai, bạn có thể chia tập hợp con của dòng số có thể đạt được theo một trong hai loại thành ba loại có kích thước bằng nhau:

- => signed only
= => both
+ => unsigned only

<..-------=======+++++++..>

Điều này có thể dễ dàng được chứng minh bằng cách xem xét đại diện. Một số nguyên không dấu bắt đầu tại 0và sử dụng tất cả các bit để tăng giá trị theo lũy thừa của 2. Một số nguyên có dấu hoàn toàn giống nhau cho tất cả các bit ngoại trừ bit dấu, giá trị này có giá trị -(2^position)thay vì 2^position. Điều này có nghĩa là đối với tất cả các n - 1bit, chúng đại diện cho các giá trị giống nhau. Sau đó, các số nguyên không dấu có thêm một bit bình thường, nhân đôi tổng số giá trị (nói cách khác, có nhiều giá trị với bit đó được đặt như không có bit đó được đặt). Logic tương tự cũng áp dụng cho các số nguyên có dấu, ngoại trừ việc tất cả các giá trị với tập bit đó đều là số âm.

Hai biểu diễn số nguyên hợp pháp khác, phần bù của một và độ lớn của dấu, có tất cả các giá trị giống như các số nguyên phần bù của hai ngoại trừ một: giá trị âm nhất. C ++ định nghĩa mọi thứ về kiểu số nguyên, ngoại trừ reinterpret_cast(và C ++ 20 std::bit_cast), về phạm vi giá trị có thể biểu diễn, không phải về biểu diễn bit. Điều này có nghĩa là phân tích của chúng tôi sẽ phù hợp với từng biểu diễn trong số ba biểu diễn này miễn là chúng tôi không cố gắng tạo biểu diễn bẫy. Giá trị không dấu sẽ ánh xạ đến giá trị bị thiếu này là một giá trị khá đáng tiếc: giá trị nằm ngay giữa các giá trị không dấu. May mắn thay, điều kiện đầu tiên của chúng tôi sẽ kiểm tra (tại thời điểm biên dịch) liệu một biểu diễn như vậy có tồn tại hay không, và sau đó xử lý nó một cách đặc biệt với kiểm tra thời gian chạy.

Điều kiện đầu tiên xử lý trường hợp chúng ta đang ở trong =phần, có nghĩa là chúng ta đang ở trong vùng chồng chéo nơi các giá trị trong một cái này có thể được biểu diễn trong cái kia mà không thay đổi. Các shift_by_windowchức năng trong tất cả các mã chuyển giá trị xuống bởi kích thước của mỗi người trong số các phân đoạn này (chúng ta phải trừ đi giá trị tối đa sau đó trừ đi 1 để tránh các vấn đề tràn toán học). Nếu chúng ta ở bên ngoài vùng đó (chúng ta ở trong +vùng), chúng ta cần phải nhảy xuống một kích thước cửa sổ. Điều này đặt chúng ta vào phạm vi chồng chéo, có nghĩa là chúng ta có thể chuyển đổi từ chưa dấu sang đã ký một cách an toàn vì không có thay đổi về giá trị. Tuy nhiên, chúng tôi vẫn chưa hoàn thành vì chúng tôi đã ánh xạ hai giá trị chưa dấu cho mỗi giá trị có dấu. Do đó, chúng ta cần chuyển xuống cửa sổ tiếp theo (- vùng) để chúng ta lại có một ánh xạ duy nhất.

Bây giờ, điều này có cung cấp cho chúng ta một mod đồng dư kết quả UINT_MAX + 1, như được yêu cầu trong câu hỏi không? UINT_MAX + 1tương đương với 2^n, ở đâu nlà số bit trong biểu diễn giá trị. Giá trị chúng tôi sử dụng cho kích thước cửa sổ của chúng tôi bằng 2^(n - 1)(chỉ số cuối cùng trong chuỗi giá trị nhỏ hơn một so với kích thước). Chúng tôi trừ giá trị đó hai lần, có nghĩa là chúng tôi trừ đi giá trị 2 * 2^(n - 1)bằng 2^n. Thêm và trừ xlà một điều không cần thiết trong mod số học x, vì vậy chúng tôi không ảnh hưởng đến mod giá trị ban đầu 2^n.

Xử lý thích hợp các quảng cáo số nguyên

Bởi vì đây là một hàm chung chứ không chỉ intunsigned, chúng ta cũng phải quan tâm đến các quy tắc thăng hạng tích phân. Có hai trường hợp có thể thú vị: một trong đó shortnhỏ hơn intvà một trong đó shortcó cùng kích thước int.

Ví dụ: shortnhỏ hơnint

Nếu shortnhỏ hơn int(phổ biến trên các nền tảng hiện đại) thì chúng tôi cũng biết rằng unsigned shortcó thể phù hợp với một int, có nghĩa là bất kỳ hoạt động nào trên nó sẽ thực sự xảy ra int, vì vậy chúng tôi rõ ràng chuyển sang loại được quảng cáo để tránh điều này. Tuyên bố cuối cùng của chúng tôi khá trừu tượng và trở nên dễ hiểu hơn nếu chúng tôi thay thế bằng các giá trị thực. Đối với trường hợp thú vị đầu tiên của chúng tôi, không mất tính tổng quát, chúng ta hãy xem xét 16 bit shortvà 17 bit int(vẫn được phép theo các quy tắc mới và chỉ có nghĩa là ít nhất một trong hai kiểu số nguyên đó có một số bit đệm ):

constexpr auto shift_by_window = [](auto x) {
    return x - static_cast<decltype(x)>(32767) - 1;
};
return static_cast<int16_t>(
    shift_by_window(
        static_cast<int17_t>(
            shift_by_window(
                static_cast<uint17_t>(value)
            )
        )
    )
);

Giải quyết cho giá trị không dấu 16 bit lớn nhất có thể

constexpr auto shift_by_window = [](auto x) {
    return x - static_cast<decltype(x)>(32767) - 1;
};
return int16_t(
    shift_by_window(
        int17_t(
            shift_by_window(
                uint17_t(65535)
            )
        )
    )
);

Đơn giản hóa thành

return int16_t(
    int17_t(
        uint17_t(65535) - uint17_t(32767) - 1
    ) -
    int17_t(32767) -
    1
);

Đơn giản hóa thành

return int16_t(
    int17_t(uint17_t(32767)) -
    int17_t(32767) -
    1
);

Đơn giản hóa thành

return int16_t(
    int17_t(32767) -
    int17_t(32767) -
    1
);

Đơn giản hóa thành

return int16_t(-1);

Chúng tôi đưa vào số lượng lớn nhất có thể chưa được ký và nhận lại -1, thành công!

Ví dụ: shortcùng kích thước vớiint

Nếu shortcó cùng kích thước với int(không phổ biến trên các nền tảng hiện đại), quy tắc thăng hạng tích hợp hơi khác một chút. Trong trường hợp này, hãy shortquảng bá tới intunsigned shortquảng bá tới unsigned. May mắn thay, chúng tôi chuyển mỗi kết quả một cách rõ ràng đến loại mà chúng tôi muốn thực hiện phép tính, vì vậy chúng tôi không có kết quả thăng hạng nào có vấn đề. Không mất tính tổng quát, chúng ta hãy xem xét 16 bit shortvà 16 bit int:

constexpr auto shift_by_window = [](auto x) {
    return x - static_cast<decltype(x)>(32767) - 1;
};
return static_cast<int16_t>(
    shift_by_window(
        static_cast<int16_t>(
            shift_by_window(
                static_cast<uint16_t>(value)
            )
        )
    )
);

Giải quyết cho giá trị không dấu 16 bit lớn nhất có thể

auto x = int16_t(
    uint16_t(65535) - uint16_t(32767) - 1
);
return int16_t(
    x - int16_t(32767) - 1
);

Đơn giản hóa thành

return int16_t(
    int16_t(32767) - int16_t(32767) - 1
);

Đơn giản hóa thành

return int16_t(-1);

Chúng tôi đưa vào số lượng lớn nhất có thể chưa được ký và nhận lại -1, thành công!

Điều gì sẽ xảy ra nếu tôi chỉ quan tâm intunsignedkhông quan tâm đến các cảnh báo, như câu hỏi ban đầu?

constexpr int cast_to_signed_integer(unsigned const value) {
    using result_limits = std::numeric_limits<int>;
    if constexpr (result_limits::min() + 1 != -result_limits::max()) {
        if (value == static_cast<unsigned>(result_limits::max()) + 1) {
            throw std::runtime_error("Cannot convert the maximum possible unsigned to a signed value on this system");
        }
    }
    if (value <= result_limits::max()) {
        return static_cast<int>(value);
    } else {
        constexpr int window = result_limits::min();
        return static_cast<int>(value + window) + window;
    }
}

Xem trực tiếp

https://godbolt.org/z/74hY81

Ở đây chúng ta thấy rằng clang, gcc và icc không tạo mã cho castcast_to_signed_integer_basictại -O2-O3và MSVC không tạo mã tại /O2, vì vậy giải pháp là tối ưu.


3

Bạn có thể nói rõ ràng với trình biên dịch những gì bạn muốn làm:

int unsigned_to_signed(unsigned n) {
  if (n > INT_MAX) {
    if (n <= UINT_MAX + INT_MIN) {
      throw "no result";
    }
    return static_cast<int>(n + INT_MIN) - (UINT_MAX + INT_MIN + 1);
  } else {
    return static_cast<int>(n);
  }
}

Biên dịch với gcc 4.7.2for x86_64-linux( g++ -O -S test.cpp) thành

_Z18unsigned_to_signedj:
    movl    %edi, %eax
    ret

UINT_MAXlà một biểu hiện của loại unsigned intvà điều đó tạo nên tổng thể static_cast<int>(n + INT_MIN) - (UINT_MAX + INT_MIN + 1)của loại đó. Tuy nhiên, có thể sửa lỗi đó và tôi hy vọng sau đó nó vẫn được biên dịch như cũ.

2

Nếu xlà đầu vào của chúng tôi ...

Nếu x > INT_MAX, chúng tôi muốn tìm một hằng số k0< x - k*INT_MAX< INT_MAX.

Điều này thật dễ dàng - unsigned int k = x / INT_MAX;. Sau đó, hãyunsigned int x2 = x - k*INT_MAX;

Bây giờ chúng ta có thể truyền x2đến intmột cách an toàn. Để choint x3 = static_cast<int>(x2);

Bây giờ chúng ta muốn trừ một số thứ như UINT_MAX - k * INT_MAX + 1từ x3, nếu k > 0.

Bây giờ, trên hệ thống bổ sung 2s, miễn là x > INT_MAX, điều này hoạt động với:

unsigned int k = x / INT_MAX;
x -= k*INT_MAX;
int r = int(x);
r += k*INT_MAX;
r -= UINT_MAX+1;

Lưu ý rằng nó UINT_MAX+1được đảm bảo bằng 0 trong C ++, chuyển đổi thành int là một noop và chúng tôi đã trừ k*INT_MAXrồi cộng lại trên "cùng một giá trị". Vì vậy, một trình tối ưu hóa có thể chấp nhận được sẽ có thể xóa tất cả những thứ đó!

Điều đó để lại vấn đề của x > INT_MAXhoặc không. Chúng ta tạo 2 nhánh, một nhánh có x > INT_MAXvà một nhánh không. Cái không có ép kiểu eo biển, mà trình biên dịch tối ưu hóa thành noop. Cái có ... phát ra tiếng kêu sau khi trình tối ưu hóa hoàn thành. Trình tối ưu hóa thông minh nhận ra cả hai nhánh giống nhau và bỏ nhánh.

Vấn đề: nếu UINT_MAXthực sự lớn so với INT_MAX, cách trên có thể không hoạt động. Tôi đang giả định điều đó k*INT_MAX <= UINT_MAX+1một cách ngầm hiểu.

Chúng tôi có thể tấn công điều này bằng một số enums như:

enum { divisor = UINT_MAX/INT_MAX, remainder = UINT_MAX-divisor*INT_MAX };

mà tôi tin rằng tính toán 2 và 1 trên hệ thống bổ sung 2s (chúng tôi có đảm bảo cho phép toán đó hoạt động không? Điều đó thật khó ...) và thực hiện logic dựa trên những điều này để dễ dàng tối ưu hóa trên các hệ thống bổ sung không phải 2s ...

Điều này cũng mở ra trường hợp ngoại lệ. Chỉ có thể nếu UINT_MAX lớn hơn nhiều so với (INT_MIN-INT_MAX), vì vậy bạn có thể đặt mã ngoại lệ của mình trong một khối if hỏi chính xác câu hỏi đó bằng cách nào đó và nó sẽ không làm chậm bạn trên hệ thống truyền thống.

Tôi không chắc chắn chính xác cách xây dựng các hằng số thời gian biên dịch đó để giải quyết chính xác điều đó.


UINT_MAXkhông thể nhỏ hơn so với INT_MAX, bởi vì spec đảm bảo rằng mọi int có dấu dương đều có thể biểu diễn dưới dạng int không dấu. Nhưng UINT_MAX+1là số không trên mọi hệ thống; số học không dấu luôn luôn là modulo UINT_MAX+1. Vẫn có thể có một hạt nhân một cách tiếp cận hoàn toàn khả thi ở đây ...
Nemo

@Nemo Chỉ theo dõi chủ đề này, vì vậy xin thứ lỗi cho câu hỏi hiển nhiên có thể xảy ra của tôi: Có phải tuyên bố của bạn " UINT_MAX+1là 0 trên mọi hệ thống` được thiết lập trong '03 -spec không? Nếu vậy, có một tiểu mục cụ thể mà tôi nên xem xét không? Cảm ơn.
WhozCraig

@WhozCraig: Mục 3.9.1 đoạn 4: "Các số nguyên không dấu, được khai báo là không dấu, sẽ tuân theo luật của modulo số học 2 ^ n trong đó n là số bit trong biểu diễn giá trị của kích thước cụ thể của số nguyên", kèm theo chú thích "Điều này ngụ ý rằng số học không dấu không tràn vì kết quả không thể được biểu diễn bằng kiểu số nguyên không dấu kết quả được giảm theo mô-đun số lớn hơn một giá trị lớn nhất có thể được biểu diễn bằng kiểu số nguyên không dấu kết quả." Về cơ bản, không dấu được chỉ định để hoạt động theo cách bạn muốn / mong đợi.
Nemo,

@Nemo Cảm ơn. đánh giá rất cao.
WhozCraig,

1

std::numeric_limits<int>::is_modulolà một hằng số thời gian biên dịch. vì vậy bạn có thể sử dụng nó cho chuyên môn hóa mẫu. vấn đề đã được giải quyết, ít nhất là nếu trình biên dịch phát cùng với nội tuyến.

#include <limits>
#include <stdexcept>
#include <string>

#ifdef TESTING_SF
    bool const testing_sf = true;
#else
    bool const testing_sf = false;
#endif

// C++ "extensions"
namespace cppx {
    using std::runtime_error;
    using std::string;

    inline bool hopefully( bool const c ) { return c; }
    inline bool throw_x( string const& s ) { throw runtime_error( s ); }

}  // namespace cppx

// C++ "portability perversions"
namespace cppp {
    using cppx::hopefully;
    using cppx::throw_x;
    using std::numeric_limits;

    namespace detail {
        template< bool isTwosComplement >
        int signed_from( unsigned const n )
        {
            if( n <= unsigned( numeric_limits<int>::max() ) )
            {
                return static_cast<int>( n );
            }

            unsigned const u_max = unsigned( -1 );
            unsigned const u_half = u_max/2 + 1;

            if( n == u_half )
            {
                throw_x( "signed_from: unsupported value (negative max)" );
            }

            int const i_quarter = static_cast<int>( u_half/2 );
            int const int_n1 = static_cast<int>( n - u_half );
            int const int_n2 = int_n1 - i_quarter;
            int const int_n3 = int_n2 - i_quarter;

            hopefully( n == static_cast<unsigned>( int_n3 ) )
                || throw_x( "signed_from: range error" );

            return int_n3;
        }

        template<>
        inline int signed_from<true>( unsigned const n )
        {
            return static_cast<int>( n );
        }
    }    // namespace detail

    inline int signed_from( unsigned const n )
    {
        bool const is_modulo = numeric_limits< int >::is_modulo;
        return detail::signed_from< is_modulo && !testing_sf >( n );
    }
}    // namespace cppp

#include <iostream>
using namespace std;
int main()
{
    int const x = cppp::signed_from( -42u );
    wcout << x << endl;
}


CHỈNH SỬA : Đã sửa mã để tránh bẫy có thể xảy ra trên các máy không có mô-đun-int (chỉ có một máy được biết là tồn tại, cụ thể là các phiên bản được cấu hình cổ của Unisys Clearpath). Để đơn giản, điều này được thực hiện bằng cách không hỗ trợ giá trị -2 n -1 trong đó n là số intbit giá trị, trên máy như vậy (tức là trên Clearpath). trong thực tế, giá trị này cũng sẽ không được máy hỗ trợ (tức là với biểu diễn dấu và độ lớn hoặc biểu diễn phần bù của 1).


1

Tôi nghĩ rằng loại int có ít nhất hai byte, vì vậy INT_MIN và INT_MAX có thể thay đổi trong các nền tảng khác nhau.

Các loại cơ bản

Tiêu đề ≤climits≥


Tôi bị nguyền rủa khi sử dụng trình biên dịch cho 6809 được định cấu hình với "-mint8" theo mặc định, trong đó int là 8 bit :-( (đây là môi trường phát triển cho Vectrex) dài là 2 byte, dài dài là 4 byte và Tôi không biết viết tắt là gì ...
Graham Toal

1

Tiền của tôi đang sử dụng memcpy. Bất kỳ trình biên dịch tốt nào cũng biết cách tối ưu hóa nó:

#include <stdio.h>
#include <memory.h>
#include <limits.h>

static inline int unsigned_to_signed(unsigned n)
{
    int result;
    memcpy( &result, &n, sizeof(result));
    return result;
}

int main(int argc, const char * argv[])
{
    unsigned int x = UINT_MAX - 1;
    int xx = unsigned_to_signed(x);
    return xx;
}

Đối với tôi (Xcode 8.3.2, Apple LLVM 8.1, -O3), tạo ra:

_main:                                  ## @main
Lfunc_begin0:
    .loc    1 21 0                  ## /Users/Someone/main.c:21:0
    .cfi_startproc
## BB#0:
    pushq    %rbp
Ltmp0:
    .cfi_def_cfa_offset 16
Ltmp1:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp2:
    .cfi_def_cfa_register %rbp
    ##DEBUG_VALUE: main:argc <- %EDI
    ##DEBUG_VALUE: main:argv <- %RSI
Ltmp3:
    ##DEBUG_VALUE: main:x <- 2147483646
    ##DEBUG_VALUE: main:xx <- 2147483646
    .loc    1 24 5 prologue_end     ## /Users/Someone/main.c:24:5
    movl    $-2, %eax
    popq    %rbp
    retq
Ltmp4:
Lfunc_end0:
    .cfi_endproc

1
Điều này không trả lời câu hỏi, vì biểu diễn nhị phân của một không dấu không phải được đảm bảo bởi tiêu chuẩn để khớp với biểu diễn có dấu.
TLW
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.