Const quá tải bất ngờ được gọi trong gcc. Trình biên dịch lỗi hoặc sửa lỗi tương thích?


8

Chúng tôi có một ứng dụng lớn hơn nhiều dựa trên quá tải mẫu của mảng char và const char. Trong gcc 7.5, clang và studio trực quan, mã bên dưới in "KHÔNG PHẢI" cho tất cả các trường hợp. Tuy nhiên, đối với gcc 8.1 trở lên, đầu ra được hiển thị bên dưới:

#include <iostream>

class MyClass
{
public:
    template <size_t N>
    MyClass(const char (&value)[N])
    {
        std::cout << "CONST " << value << '\n';
    }

    template <size_t N>
    MyClass(char (&value)[N])
    {
        std::cout << "NON-CONST " << value << '\n';
    }
};

MyClass test_1()
{
    char buf[30] = "test_1";
    return buf;
}

MyClass test_2()
{
    char buf[30] = "test_2";
    return {buf};
}

void test_3()
{
    char buf[30] = "test_3";
    MyClass x{buf};
}

void test_4()
{
    char buf[30] = "test_4";
    MyClass x(buf);
}

void test_5()
{
    char buf[30] = "test_5";
    MyClass x = buf;
}

int main()
{
    test_1();
    test_2();
    test_3();
    test_4();
    test_5();
}

Đầu ra gcc 8 và 9 (từ godbolt) là:

CONST test_1
NON-CONST test_2
NON-CONST test_3
NON-CONST test_4
NON-CONST test_5

Đây có vẻ là một lỗi biên dịch, nhưng tôi đoán nó có thể là một số vấn đề khác liên quan đến thay đổi ngôn ngữ. Có ai biết dứt khoát không?


Bạn đã thử biên dịch với các phiên bản tiêu chuẩn C ++ khác nhau chưa?
n314159

1
g ++ và clang
Ted Lyngmo

@ n314159 Câu hỏi hay, tôi vừa làm. -std = c ++ 11 -std = c ++ 14 -std = c ++ 17 và -std = c ++ 2a đều tạo ra kết quả "xấu" như nhau. Không biên dịch với -std = c ++ 03.
Rob L

1
@TedLyngmo vâng, tôi lưu ý rằng clang hoạt động như tôi mong đợi, cũng như studio hình ảnh.
Rob L

Câu trả lời:


6

Khi bạn trả về một biểu thức id đơn giản từ một hàm (đã chỉ định một đối tượng cục bộ của hàm), trình biên dịch được ủy nhiệm thực hiện quá trình giải quyết quá tải hai lần. Đầu tiên, nó đối xử với nó như thể nó là một giá trị, và không phải là một giá trị. Chỉ khi độ phân giải quá tải đầu tiên thất bại, nó sẽ được thực hiện lại với đối tượng dưới dạng giá trị.

[class.copy.elision]

3 Trong các bối cảnh khởi tạo sao chép sau đây, thao tác di chuyển có thể được sử dụng thay cho thao tác sao chép:

  • Nếu biểu thức trong câu lệnh return là biểu thức id (có thể được ngoặc đơn) đặt tên một đối tượng có thời lượng lưu trữ tự động được khai báo trong phần thân hoặc mệnh đề khai báo tham số của hàm bao trong cùng hoặc biểu thức lambda, hoặc

  • ...

độ phân giải quá tải để chọn hàm tạo cho bản sao được thực hiện trước tiên như thể đối tượng được chỉ định bởi một giá trị. Nếu độ phân giải quá tải đầu tiên không thành công hoặc không được thực hiện hoặc nếu loại tham số đầu tiên của hàm tạo được chọn không phải là tham chiếu giá trị cho loại của đối tượng (có thể đủ điều kiện cv), thì độ phân giải quá tải được thực hiện lại, coi đối tượng là một giá trị [Lưu ý: Độ phân giải quá tải hai giai đoạn này phải được thực hiện bất kể việc sao chép có xảy ra hay không. Nó xác định hàm tạo được gọi nếu elision không được thực hiện và hàm tạo được chọn phải có thể truy cập được ngay cả khi cuộc gọi bị xóa. - lưu ý cuối]

Nếu chúng ta thêm một quá tải giá trị,

template <size_t N>
MyClass (char (&&value)[N])
{
    std::cout << "RVALUE " << value << '\n';
}

đầu ra sẽ trở thành

RVALUE test_1
NON-CONST test_2
NON-CONST test_3
NON-CONST test_4
NON-CONST test_5

và điều này sẽ đúng Điều không đúng là hành vi của GCC như bạn thấy. Nó coi việc giải quyết quá tải đầu tiên là một thành công. Đó là bởi vì một tham chiếu const lvalue có thể liên kết với một giá trị. Tuy nhiên, nó bỏ qua văn bản "hoặc nếu loại tham số đầu tiên của hàm tạo được chọn không phải là tham chiếu giá trị cho loại của đối tượng" . Theo đó, nó phải loại bỏ kết quả của độ phân giải quá tải đầu tiên, và làm lại.

Chà, dù sao thì đó cũng là tình huống lên tới C ++ 17. Dự thảo tiêu chuẩn hiện tại nói một cái gì đó khác nhau.

Nếu độ phân giải quá tải đầu tiên không thành công hoặc không được thực hiện, độ phân giải quá tải được thực hiện lại, coi biểu thức hoặc toán hạng là giá trị.

Văn bản từ tối đa C ++ 17 đã bị xóa. Vì vậy, đó là một lỗi du hành thời gian. GCC thực hiện hành vi C ++ 20, nhưng nó thực hiện ngay cả khi tiêu chuẩn là C ++ 17.


Cảm ơn bạn! Có vẻ như bạn cũng đã cho tôi một cách giải quyết hoạt động trong trường hợp cụ thể này, thêm quá tải giá trị. Tôi chỉ có thể làm cho nó giống hệt với phiên bản char &.
Rob L

1
@RobL - Rất vui được giúp đỡ. Mặc dù xin lưu ý rằng tình hình có nhiều sắc thái hơn, sau đó tôi nghĩ ban đầu. Các văn bản đã thực sự thay đổi. Tôi rất vui vì đã kiểm tra.
Người kể chuyện - Unslander Monica

Tôi đoán điều đó có nghĩa là clang++việc triển khai C ++ 20 cũng không đúng vì nó sử dụng phiên bản NON-CONST trong mọi trường hợp trong mã gốc.
Ted Lyngmo

2
@TedLyngmo - Với vấn đề du hành thời gian, đó thực sự là vấn đề khi nào. Tôi tưởng tượng các nhà phát triển Clang chỉ không thực hiện được thay đổi này. Tôi sẽ không gọi nó là một lỗi mỗi se. GCC làm mới trong C ++ 17 có lẽ là một lỗi. Phụ thuộc vào cách thay đổi này được đưa vào tiêu chuẩn. Tôi không tin rằng có một báo cáo lỗi sẽ yêu cầu điều này thay đổi hồi tố, vì vậy tôi cho rằng đó là lỗi GCC. Hỗ trợ nhiều tiêu chuẩn là công việc sắc thái.
Người kể chuyện - Unslander Monica

1
@RobL - Đó là một đối tượng địa phương chức năng. Sau khi tuyên bố trở lại, nó đã biến mất. Đây là một điểm tối ưu hóa có chủ ý. Thay vì sao chép, đối tượng có thể bị ăn thịt. Thực tiễn tiêu chuẩn là viết các loại hành xử chính xác cho bất kỳ loại giá trị nào chúng được trao.
Người kể chuyện - Unslander Monica

0

Có một cuộc tranh luận về việc liệu đây có phải là "hành vi trực quan" trong các bình luận hay không, vì vậy tôi nghĩ rằng tôi sẽ đâm đầu vào lý do đằng sau hành vi này.

Có một cuộc nói chuyện khá hay được đưa ra tại CPPCON, điều này làm cho tôi rõ hơn một chút về { talk , slide }. Về cơ bản, một hàm có tham chiếu không phải là gì? Rằng đối tượng đầu vào phải được đọc / ghi . Thậm chí mạnh hơn, nó ngụ ý tôi có ý định sửa đổi đối tượng này, chức năng này có tác dụng phụ . Một ref ref ngụ ý chỉ đọc và rvalue ref có nghĩa là tôi có thể lấy tài nguyên . Nếu test_1()cuối cùng gọi cho nhà NON-CONSTxây dựng, điều đó có nghĩa là tôi có ý định sửa đổi đối tượng này, mặc dù sau khi tôi hoàn thành nó không còn tồn tại,mà (tôi nghĩ) sẽ là một lỗi (Tôi đang nghĩ đến một trường hợp trong đó cách tham chiếu bị ràng buộc trong quá trình khởi tạo phụ thuộc vào việc đối số được truyền vào có phải là const hay không).

Điều liên quan hơn một chút với tôi là sự tinh tế được giới thiệu bởi test_2(). Ở đây, việc khởi tạo danh sách sao chép đang diễn ra thay vì các quy tắc liên quan đến [class.copy.elision] được trích dẫn ở trên. Bây giờ bạn thực sự đang nói trả về một đối tượng thuộc loại MyClass như thể tôi đã khởi tạo nó với buf, vì vậy NON-CONSThành vi được gọi. Tôi đã luôn nghĩ về danh sách init là cách ngắn gọn hơn, nhưng ở đây, niềng răng tạo ra sự khác biệt đáng kể về ngữ nghĩa. Điều này sẽ quan trọng hơn nếu các nhà xây dựng MyClassđã lấy một số lượng lớn các đối số. Sau đó, giả sử bạn muốn tạo một buf, sửa đổi nó, sau đó trả lại với số lượng lớn các đối số, gọi CONSThành vi. Ví dụ: giả sử bạn có các hàm tạo:

template <size_t N>
MyClass(const char (&value)[N], int)
{
    std::cout << "CONST int " << value << '\n';
}

template <size_t N>
MyClass(char (&value)[N], int)
{
    std::cout << "NON-CONST int " << value << '\n';
}

Và kiểm tra:

MyClass test_0() {
    char buf[30] = "test_0";
    return {buf,0};
}

Godbolt nói với chúng ta rằng chúng ta có NON-CONSThành vi, mặc dù CONSTcó lẽ là những gì chúng ta muốn (sau khi bạn đã uống chất trợ giúp mát mẻ về ngữ nghĩa đối số chức năng). Nhưng bây giờ, việc khởi tạo danh sách bản sao không làm những gì chúng ta muốn. Các loại thử nghiệm sau đây làm cho quan điểm của tôi tốt hơn:

MyClass test_0() {
    char buf[30] = "test_0";
    buf[0] = 'T';
    const char (&bufR)[30]{buf};
    return {bufR,0};
}
// OUTPUT: CONST int Test_0

Bây giờ để có được ngữ nghĩa phù hợp với khởi tạo danh sách sao chép, bộ đệm cần phải được "bật lại" ở cuối. Tôi đoán nếu mục tiêu là đối tượng này là để khởi tạo một số MyClassđối tượng khác , chỉ cần sử dụng NON-CONSThành vi trong danh sách sao chép trở lại sẽ ổn nếu công cụ di chuyển / sao chép đã gọi bất kỳ hành vi thích hợp nào, nhưng điều đó có vẻ khá hay mong manh.

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.