Cộng đồng C++ tranh luận về việc thiết kế std::adjacent_difference có phải là một sai lầm

Nhóm Cộng đồng BigGo
Cộng đồng C++ tranh luận về việc thiết kế std::adjacent_difference có phải là một sai lầm

Cộng đồng lập trình C++ đang tham gia vào một cuộc thảo luận sôi nổi về một trong những quyết định thiết kế gây tranh cãi nhất của Standard Template Library ( STL ). Trung tâm của cuộc tranh luận là std::adjacent_difference, một thuật toán tính toán sự khác biệt giữa các phần tử lân cận trong một chuỗi nhưng với một điểm đặc biệt đã làm các nhà phát triển bực bội trong nhiều thập kỷ.

Vấn đề cốt lõi với khả năng tương thích kiểu dữ liệu

Vấn đề chính xuất phát từ cách std::adjacent_difference xử lý đầu ra của nó. Không giống như những gì nhiều người mong đợi, thuật toán này không chỉ tính toán sự khác biệt - nó còn sao chép phần tử đầu tiên của chuỗi đầu vào không thay đổi vào đầu ra. Lựa chọn thiết kế tưởng chừng vô tội này tạo ra những vấn đề đáng kể khi làm việc với các kiểu dữ liệu khác nhau.

Các thành viên cộng đồng đã nêu bật cách điều này buộc kiểu đầu ra phải khớp với kiểu đầu vào, điều này thường không có ý nghĩa về mặt toán học. Khi bạn trừ hai dấu thời gian, bạn được một khoảng thời gian, không phải một dấu thời gian khác. Khi bạn trừ hai số nguyên không dấu, bạn có thể cần một số nguyên có dấu để xử lý kết quả âm. Thiết kế hiện tại làm cho những thao tác phổ biến này trở nên không thể hoặc khó thực hiện.

Vấn đề tương thích kiểu dữ liệu

  • Timestamps: timestamp - timestamp = duration (các kiểu khác nhau)
  • Số nguyên không dấu: Có thể yêu cầu đầu ra có dấu cho các hiệu số âm
  • Kiểu tùy chỉnh: Các phép toán hiệu thường tạo ra các kiểu kết quả khác nhau
  • Hạn chế hiện tại: Đầu ra phải khớp với kiểu đầu vào trong std::adjacent_difference

Lý luận toán học đằng sau thiết kế

Alex Stepanov , người tạo ra STL , không đưa ra lựa chọn này một cách tình cờ. Thiết kế đảm bảo rằng std::adjacent_differencestd::partial_sum hoạt động như nghịch đảo toán học của nhau. Điều này tạo ra một sự đối xứng tao nhã nơi bạn có thể tính toán sự khác biệt từ một chuỗi, sau đó sử dụng tổng từng phần để tái tạo dữ liệu gốc một cách hoàn hảo.

Kết nối này sâu sắc hơn chỉ là sự tiện lợi trong lập trình. Thiết kế phản ánh các khái niệm từ giải tích, nơi đạo hàm và tích phân có mối quan hệ nghịch đảo. Trong toán học rời rạc, sự khác biệt lân cận đóng vai trò như đạo hàm trong khi tổng từng phần hoạt động như tích phân. Phần tử đầu tiên được bảo tồn hoạt động như hằng số tích phân xuất hiện khi tính toán tích phân bất định.

Các phương pháp và giải pháp thay thế

Cộng đồng lập trình đã đề xuất một số giải pháp để giải quyết những hạn chế này. Một gợi ý liên quan đến việc làm cho partial_sum nhận một tham số tích lũy ban đầu, điều này sẽ khôi phục sự đối xứng trong khi cho phép các kiểu đầu vào và đầu ra khác nhau. Phương pháp này sẽ giải quyết vấn đề không khớp kiểu trong khi duy trì sự tao nhã toán học mà Stepanov dự định.

Tôi nghĩ điều này cũng bởi vì C++ không có khái niệm chung về 'số không'; nếu không thì người ta có thể định nghĩa phần tử đầu tiên của adjacent_difference(v) là v(1)- zero<typeof(v)>, và nó sẽ ổn định về kiểu.

Một số nhà phát triển đã tìm thấy cảm hứng từ các ngôn ngữ lập trình khác. Ngôn ngữ lập trình q cung cấp một hàm deltas đạt được các tính chất toán học tương tự trong khi tránh các vấn đề tương thích kiểu. Thay vì sao chép trực tiếp phần tử đầu tiên, nó thêm một giá trị không trước khi tính toán sự khác biệt, duy trì mối quan hệ nghịch đảo với tổng từng phần mà không ép buộc ràng buộc kiểu.

So sánh các thuật toán STL chính

Thuật toán Loại đầu vào Loại đầu ra Bảo toàn phần tử đầu tiên
std::adjacent_difference T T (bắt buộc)
std::partial_sum T T Không
Hàm deltas trong ngôn ngữ q T T Có (dưới dạng số không)

Tác động rộng hơn đến C++ hiện đại

Cuộc tranh luận này phản ánh những câu hỏi lớn hơn về thiết kế API và khả năng tương thích ngược trong các ngôn ngữ lập trình. Trong khi C++ hiện đại đã giới thiệu các tính năng như T{} để khởi tạo số không, std::adjacent_difference gốc vẫn không thay đổi vì lý do tương thích. Nhiều nhà phát triển đã phải viết các vòng lặp tùy chỉnh hoặc hàm bao bọc để giải quyết những hạn chế này.

Cuộc thảo luận cũng đề cập đến các quyết định thiết kế STL gây tranh cãi khác, với các thành viên cộng đồng rút ra những điểm tương đồng với std::vector<bool>, một chuyên biệt hóa khác được chỉ trích rộng rãi vì phá vỡ hành vi container mong đợi. Những ví dụ này làm nổi bật những thách thức trong việc cân bằng giữa sự tao nhã toán học với khả năng sử dụng thực tế trong thiết kế thư viện.

Cuộc tranh luận đang diễn ra cho thấy cách các quyết định thiết kế được đưa ra từ nhiều thập kỷ trước tiếp tục ảnh hưởng đến các thực hành lập trình hiện đại. Trong khi lựa chọn của Stepanov tạo ra vẻ đẹp toán học thông qua mối quan hệ nghịch đảo giữa sự khác biệt lân cận và tổng từng phần, nó cũng giới thiệu những hạn chế thực tế mà các nhà phát triển vẫn phải vật lộn đến ngày nay. Liệu điều này có đại diện cho một lỗi thiết kế cơ bản hay một sự đánh đổi có thể chấp nhận được cho sự tao nhã toán học vẫn là vấn đề quan điểm trong cộng đồng C++ .

Tham khảo: Stepanov's biggest blunder