Tại sao không có biến đổi_if trong thư viện chuẩn C ++?


82

Một ca sử dụng xuất hiện khi muốn thực hiện một bản sao theo điều kiện (1. doable with copy_if) nhưng từ một vùng chứa các giá trị đến một vùng chứa các con trỏ đến các giá trị đó (2. doable with transform).

Với các công cụ có sẵn, tôi không thể làm điều đó trong ít hơn hai bước:

#include <vector>
#include <algorithm>

using namespace std;

struct ha { 
    int i;
    explicit ha(int a) : i(a) {}
};

int main() 
{
    vector<ha> v{ ha{1}, ha{7}, ha{1} }; // initial vector
    // GOAL : make a vector of pointers to elements with i < 2
    vector<ha*> ph; // target vector
    vector<ha*> pv; // temporary vector
    // 1. 
    transform(v.begin(), v.end(), back_inserter(pv), 
        [](ha &arg) { return &arg; }); 
    // 2. 
    copy_if(pv.begin(), pv.end(), back_inserter(ph),
        [](ha *parg) { return parg->i < 2;  }); // 2. 

    return 0;
}

Ofcourse chúng ta có thể gọi remove_ifvào pvvà loại bỏ sự cần thiết cho một tạm thời, nhưng vẫn tốt hơn, mặc dù nó không phải là khó khăn để thực hiện (đối với hoạt động unary) một cái gì đó như thế này:

template <
    class InputIterator, class OutputIterator, 
    class UnaryOperator, class Pred
>
OutputIterator transform_if(InputIterator first1, InputIterator last1,
                            OutputIterator result, UnaryOperator op, Pred pred)
{
    while (first1 != last1) 
    {
        if (pred(*first1)) {
            *result = op(*first1);
            ++result;
        }
        ++first1;
    }
    return result;
}

// example call 
transform_if(v.begin(), v.end(), back_inserter(ph), 
[](ha &arg) { return &arg;      }, // 1. 
[](ha &arg) { return arg.i < 2; });// 2.
  1. Có giải pháp nào thanh lịch hơn với các công cụ thư viện chuẩn C ++ có sẵn không?
  2. Có một lý do tại sao transform_ifkhông tồn tại trong thư viện? Sự kết hợp của các công cụ hiện có có phải là một giải pháp đầy đủ và / hoặc được coi là hiệu suất hoạt động tốt không?

(IMO) Tên transform_ifngụ ý "chỉ biến đổi nếu một vị từ nhất định được thỏa mãn". Một tên mô tả hơn cho những gì bạn muốn sẽ là copy_if_and_transform!
Oliver Charlesworth

@OliCharlesworth, thực ra copy_ifcũng ngụ ý "chỉ sao chép nếu một vị từ nhất định được thỏa mãn". Nó cũng mơ hồ không kém.
Shahbaz

@Shahbaz: Nhưng đó là những gì copy_if, phải không?
Oliver Charlesworth

2
Tôi sẽ không ngạc nhiên nếu những tranh chấp về tên của một thứ như vậy là lý do cơ bản để không thực hiện nó !!
Nikos Athanasiou

6
Có thể tôi đang thiếu một cái gì đó trong những nhận xét này, nhưng làm thế nào có transform_ifthể sao chép những yếu tố đó mà nó không biến đổi, nếu sự chuyển đổi có thể là một loại không tương thích khác? Việc triển khai trong câu hỏi là chính xác những gì tôi mong đợi một chức năng như vậy thực hiện.

Câu trả lời:


32

Thư viện tiêu chuẩn ủng hộ các thuật toán cơ bản.

Các vùng chứa và thuật toán nên độc lập với nhau nếu có thể.

Tương tự như vậy, các thuật toán có thể bao gồm các thuật toán hiện có chỉ hiếm khi được đưa vào, dưới dạng tốc ký.

Nếu bạn yêu cầu một phép chuyển đổi if, bạn có thể viết nó một cách nhẹ nhàng. Nếu bạn muốn nó / today /, soạn các mã sẵn sàng và không phải trả chi phí, bạn có thể sử dụng thư viện phạm vi có phạm vi lười biếng , chẳng hạn như Boost.Range , ví dụ:

v | filtered(arg1 % 2) | transformed(arg1 * arg1 / 7.0)

Như @hvd đã chỉ ra trong một nhận xét, transform_ifgấp đôi kết quả là một kiểu khác ( doubletrong trường hợp này). Thứ tự thành phần rất quan trọng và với Boost Range, bạn cũng có thể viết:

 v | transformed(arg1 * arg1 / 7.0) | filtered(arg1 < 2.0)

dẫn đến ngữ nghĩa khác nhau. Điều này thúc đẩy quan điểm về nhà:

nó làm cho cảm giác rất ít để bao gồm std::filter_and_transform, std::transform_and_filter, std::filter_transform_and_filtervv vv vào thư viện chuẩn .

Xem mẫu trực tiếp trên Coliru

#include <boost/range/algorithm.hpp>
#include <boost/range/adaptors.hpp>

using namespace boost::adaptors;

// only for succinct predicates without lambdas
#include <boost/phoenix.hpp>
using namespace boost::phoenix::arg_names;

// for demo
#include <iostream>

int main()
{
    std::vector<int> const v { 1,2,3,4,5 };

    boost::copy(
            v | filtered(arg1 % 2) | transformed(arg1 * arg1 / 7.0),
            std::ostream_iterator<double>(std::cout, "\n"));
}

26
Chà, vấn đề là các thuật toán tiêu chuẩn không thể dễ dàng tạo ra, bởi vì chúng không lười biếng.
Jan Hudec

1
@JanHudec Thật vậy. (xin lỗi vì điều đó? :)). Đó là lý do tại sao bạn sử dụng thư viện (giống như bạn sử dụng AMP / TBB cho đồng thời hoặc Tiện ích mở rộng phản ứng trong C #). Nhiều người đang làm việc trên một đề xuất phạm vi + triển khai để đưa vào tiêu chuẩn.
sehe

2
@sehe +1 Rất ấn tượng, hôm nay tôi đã học được một điều mới! Bạn có vui lòng cho chúng tôi biết những người không quen thuộc với Boost.Range và Phoenix nơi chúng tôi có thể tìm thấy tài liệu / ví dụ giải thích cách sử dụng boost::phoenixđể tạo các vị từ tốt đẹp như vậy mà không có lambdas không? Một tìm kiếm nhanh trên google không trả lại gì có liên quan. Cảm ơn!
Ali

1
Tôi không đồng ý về phần "rất ít ý nghĩa khi bao gồm std :: filter_and_transform". Các ngôn ngữ lập trình khác cũng cung cấp sự kết hợp này trong "thư viện chuẩn" của chúng. Hoàn toàn có ý nghĩa khi lặp lại danh sách các phần tử một lần, biến đổi chúng ngay lập tức, trong khi bỏ qua những phần tử không thể chuyển đổi. Các cách tiếp cận khác yêu cầu nhiều hơn một lần vượt qua. Có, bạn có thể sử dụng BOOST, nhưng câu hỏi thực sự là "Tại sao không có biến đổi_if trong thư viện chuẩn C ++?". Và IMHO, anh ấy đã đúng khi đặt câu hỏi về điều này. Nên có một chức năng như vậy trong thư viện chuẩn.
Jonny Dee

1
@sehe Về việc "tất cả đều sử dụng trừu tượng tổng hợp": điều đó không đúng. Rust, chẳng hạn, có chính xác như vậy transform_if. Nó được gọi là filter_map. Tuy nhiên, tôi phải thừa nhận rằng nó ở đó để đơn giản hóa mã, nhưng mặt khác, người ta có thể áp dụng cùng một đối số trong trường hợp C ++.
Jonny Dee

6

Ký hiệu vòng lặp for mới theo nhiều cách làm giảm nhu cầu về các thuật toán truy cập vào mọi phần tử của tập hợp, nơi giờ đây chỉ cần viết một vòng lặp và đặt logic vào vị trí đã gọn gàng hơn.

std::vector< decltype( op( begin(coll) ) > output;
for( auto const& elem : coll )
{
   if( pred( elem ) )
   {
        output.push_back( op( elem ) );
   }
}

Bây giờ nó có thực sự cung cấp nhiều giá trị để đưa vào một thuật toán không? Trong khi có, thuật toán sẽ hữu ích cho C ++ 03 và thực sự tôi đã có một thuật toán cho nó, chúng tôi không cần một thuật toán bây giờ nên không có lợi thế thực sự trong việc thêm nó.

Lưu ý rằng trong thực tế sử dụng mã của bạn không phải lúc nào cũng giống hệt như vậy: bạn không nhất thiết phải có các hàm "op" và "pred" và có thể phải tạo lambdas để làm cho chúng "phù hợp" với các thuật toán. Mặc dù rất tốt nếu tách ra các mối quan tâm nếu logic phức tạp, nếu nó chỉ là vấn đề trích xuất một phần tử từ kiểu đầu vào và kiểm tra giá trị của nó hoặc thêm nó vào bộ sưu tập, thì lại đơn giản hơn rất nhiều so với sử dụng thuật toán.

Ngoài ra, khi bạn đang thêm một số loại biến đổi, bạn phải quyết định xem có nên áp dụng vị từ trước hay sau biến đổi hay thậm chí có 2 vị từ và áp dụng nó ở cả hai nơi.

Vậy chúng ta sẽ làm gì? Thêm 3 thuật toán? (Và trong trường hợp trình biên dịch có thể áp dụng vị từ ở một trong hai đầu của chuyển đổi, người dùng có thể dễ dàng chọn sai thuật toán do nhầm lẫn và mã vẫn biên dịch nhưng tạo ra kết quả sai).

Ngoài ra, nếu các bộ sưu tập lớn, người dùng có muốn lặp lại với các trình vòng lặp hoặc ánh xạ / giảm bớt không? Với sự ra đời của bản đồ / rút gọn, bạn thậm chí còn thấy phức tạp hơn trong phương trình.

Về cơ bản, thư viện cung cấp các công cụ và người dùng được để ở đây để sử dụng chúng để phù hợp với những gì họ muốn làm, chứ không phải ngược lại như thường thấy với các thuật toán. (Xem cách người dùng ở trên cố gắng xoay chuyển mọi thứ bằng cách sử dụng tích lũy để phù hợp với những gì họ thực sự muốn làm).

Ví dụ đơn giản, một bản đồ. Đối với mỗi phần tử, tôi sẽ xuất ra giá trị nếu khóa là chẵn.

std::vector< std::string > valuesOfEvenKeys
    ( std::map< int, std::string > const& keyValues )
{
    std::vector< std::string > res;
    for( auto const& elem: keyValues )
    {
        if( elem.first % 2 == 0 )
        {
            res.push_back( elem.second );
        }
    }
    return res;
}         

Đẹp và đơn giản. Bạn có thích điều chỉnh nó thành một thuật toán biến đổi không?


3
Nếu bạn cho rằng đoạn mã của tôi ở trên có nhiều chỗ cho lỗi hơn là một biến_nếu với 2 lambdas, một cho vị từ và một cho biến đổi, thì vui lòng giải thích nó. Assembly, C và C ++ là các ngôn ngữ khác nhau và có những chỗ khác nhau. Điểm duy nhất mà thuật toán có thể có lợi hơn một vòng lặp là khả năng "ánh xạ / thu nhỏ" để chạy đồng thời trên các tập hợp lớn. Tuy nhiên, bằng cách này người dùng có thể kiểm soát việc lặp lại theo trình tự hay giảm bản đồ.
CashCow

3
Trong một cách tiếp cận chức năng thích hợp, các hàm cho vị từ và bộ biến đổi là các khối được xác định rõ ràng giúp cấu trúc có cấu trúc đúng. Đối với phần thân vòng lặp có thể có nhiều thứ tùy ý trong đó và mỗi vòng lặp bạn nhìn thấy phải được phân tích cẩn thận để hiểu hành vi của nó.
Bartek Banachewicz

2
Để lại cách tiếp cận chức năng thích hợp cho các ngôn ngữ chức năng thích hợp. Đây là C ++.
CashCow

3
"Bạn có thích điều chỉnh nó thành một thuật toán biến đổi không?" Đó một "thuật toán biến đổi", ngoại trừ nó có mọi thứ được mã hóa cứng.
R. Martinho Fernandes

2
Nó thực hiện tương đương với một biến đổi_if. Chỉ là các thuật toán được cho là đơn giản hóa mã của bạn hoặc cải thiện nó theo một cách nào đó, không làm cho nó phức tạp hơn.
CashCow

5

Xin lỗi để trả lời câu hỏi này sau một thời gian dài. Tôi đã có một yêu cầu tương tự gần đây. Tôi đã giải quyết nó bằng cách viết một phiên bản back_insert_iterator có một sự tăng cường :: tùy chọn:

template<class Container>
struct optional_back_insert_iterator
: public std::iterator< std::output_iterator_tag,
void, void, void, void >
{
    explicit optional_back_insert_iterator( Container& c )
    : container(std::addressof(c))
    {}

    using value_type = typename Container::value_type;

    optional_back_insert_iterator<Container>&
    operator=( const boost::optional<value_type> opt )
    {
        if (opt) {
            container->push_back(std::move(opt.value()));
        }
        return *this;
    }

    optional_back_insert_iterator<Container>&
    operator*() {
        return *this;
    }

    optional_back_insert_iterator<Container>&
    operator++() {
        return *this;
    }

    optional_back_insert_iterator<Container>&
    operator++(int) {
        return *this;
    }

protected:
    Container* container;
};

template<class Container>
optional_back_insert_iterator<Container> optional_back_inserter(Container& container)
{
    return optional_back_insert_iterator<Container>(container);
}

được sử dụng như thế này:

transform(begin(s), end(s),
          optional_back_inserter(d),
          [](const auto& s) -> boost::optional<size_t> {
              if (s.length() > 1)
                  return { s.length() * 2 };
              else
                  return { boost::none };
          });

1
Không được đo lường - cho đến khi người dùng phàn nàn rằng trải nghiệm của họ bị ràng buộc bởi CPU (tức là không bao giờ), tôi quan tâm đến độ chính xác hơn là nano giây. Tuy nhiên tôi không thể thấy nó kém. Tùy chọn rất rẻ vì không có cấp phát bộ nhớ và hàm tạo Ts chỉ được gọi nếu tùy chọn thực sự được điền. Tôi hy vọng trình tối ưu hóa sẽ loại bỏ hầu hết tất cả mã chết vì tất cả các đường dẫn mã đều có thể nhìn thấy tại thời điểm biên dịch.
Richard Hodges

Vâng. Tôi đồng ý nếu nó không chính xác về một thuật toán mục đích chung (thực ra, khối xây dựng chung bên trong chúng). Đây là nơi mà tôi thường không thích thú trừ khi một thứ gì đó đơn giản như nó có. Hơn nữa, tôi muốn xử lý tùy chọn để trở thành trình trang trí trên bất kỳ trình vòng lặp đầu ra nào (vì vậy ít nhất chúng tôi có được khả năng kết hợp của các trình vòng lặp đầu ra, trong khi chúng tôi đang cố gắng bổ sung sự thiếu khả năng kết hợp của các thuật toán).
sehe

Về mặt logic, không có sự khác biệt nào cho dù bạn xử lý chèn tùy chọn thông qua trình trang trí trên vòng lặp hoặc trong hàm chuyển đổi. Cuối cùng nó chỉ là một thử nghiệm của một lá cờ. Tôi nghĩ rằng bạn sẽ thấy rằng mã được tối ưu hóa sẽ giống nhau. Điều duy nhất cản trở việc tối ưu hóa hoàn toàn sẽ là xử lý ngoại lệ. Đánh dấu T là không có hàm tạo không chấp nhận sẽ chữa được điều này.
Richard Hodges

bạn muốn gọi biến đổi () ở dạng nào? Tôi chắc rằng chúng ta có thể xây dựng một bộ trình lặp có thể kết hợp.
Richard Hodges

Tôi cũng vậy :) Tôi đã bình luận về đề xuất của bạn. Tôi đã không đề xuất điều gì khác (tôi đã có điều đó từ lâu. Thay vào đó, hãy có phạm vi và thuật toán có thể kết hợp :))
xem

3

Tiêu chuẩn này được thiết kế theo cách để giảm thiểu sự trùng lặp.

Trong trường hợp cụ thể này, bạn có thể đạt được mục tiêu của algoritm theo cách dễ đọc và ngắn gọn hơn với vòng lặp range-for đơn giản.

// another way

vector<ha*> newVec;
for(auto& item : v) {
    if (item.i < 2) {
        newVec.push_back(&item);
    }
}

Tôi đã sửa đổi ví dụ để nó biên dịch, thêm một số chẩn đoán và trình bày cả thuật toán của OP và của tôi cạnh nhau.

#include <vector>
#include <algorithm>
#include <iostream>
#include <iterator>

using namespace std;

struct ha { 
    explicit ha(int a) : i(a) {}
    int i;   // added this to solve compile error
};

// added diagnostic helpers
ostream& operator<<(ostream& os, const ha& t) {
    os << "{ " << t.i << " }";
    return os;
}

ostream& operator<<(ostream& os, const ha* t) {
    os << "&" << *t;
    return os;
}

int main() 
{
    vector<ha> v{ ha{1}, ha{7}, ha{1} }; // initial vector
    // GOAL : make a vector of pointers to elements with i < 2
    vector<ha*> ph; // target vector
    vector<ha*> pv; // temporary vector
    // 1. 
    transform(v.begin(), v.end(), back_inserter(pv), 
        [](ha &arg) { return &arg; }); 
    // 2. 
    copy_if(pv.begin(), pv.end(), back_inserter(ph),
        [](ha *parg) { return parg->i < 2;  }); // 2. 

    // output diagnostics
    copy(begin(v), end(v), ostream_iterator<ha>(cout));
    cout << endl;
    copy(begin(ph), end(ph), ostream_iterator<ha*>(cout));
    cout << endl;


    // another way

    vector<ha*> newVec;
    for(auto& item : v) {
        if (item.i < 2) {
            newVec.push_back(&item);
        }
    }

    // diagnostics
    copy(begin(newVec), end(newVec), ostream_iterator<ha*>(cout));
    cout << endl;
    return 0;
}

3

Sau khi chỉ tìm lại câu hỏi này sau một thời gian và nghĩ ra một loạt các bộ điều hợp trình vòng lặp chung hữu ích tiềm năng, tôi nhận ra rằng câu hỏi ban đầu yêu cầu KHÔNG GÌ nhiều hơn std::reference_wrapper.

Sử dụng nó thay vì một con trỏ, và bạn tốt:

Live On Coliru

#include <algorithm>
#include <functional> // std::reference_wrapper
#include <iostream>
#include <vector>

struct ha {
    int i;
};

int main() {
    std::vector<ha> v { {1}, {7}, {1}, };

    std::vector<std::reference_wrapper<ha const> > ph; // target vector
    copy_if(v.begin(), v.end(), back_inserter(ph), [](const ha &parg) { return parg.i < 2; });

    for (ha const& el : ph)
        std::cout << el.i << " ";
}

Bản in

1 1 

1

Bạn có thể sử dụng copy_ifcùng. Tại sao không? Xác định OutputIt(xem bản sao ):

struct my_inserter: back_insert_iterator<vector<ha *>>
{
  my_inserter(vector<ha *> &dst)
    : back_insert_iterator<vector<ha *>>(back_inserter<vector<ha *>>(dst))
  {
  }
  my_inserter &operator *()
  {
    return *this;
  }
  my_inserter &operator =(ha &arg)
  {
    *static_cast< back_insert_iterator<vector<ha *>> &>(*this) = &arg;
    return *this;
  }
};

và viết lại mã của bạn:

int main() 
{
    vector<ha> v{ ha{1}, ha{7}, ha{1} }; // initial vector
    // GOAL : make a vector of pointers to elements with i < 2
    vector<ha*> ph; // target vector

    my_inserter yes(ph);
    copy_if(v.begin(), v.end(), yes,
        [](const ha &parg) { return parg.i < 2;  });

    return 0;
}

3
"Tại sao không?" - Vì mã là dành cho con người. Đối với tôi, ma sát thực sự còn tệ hơn việc quay lại viết các đối tượng hàm thay vì lambdas. *static_cast< back_insert_iterator<vector<ha *>> &>(*this) = &arg;vừa khó đọc vừa cụ thể. Xem c ++ 17 này với các cách sử dụng chung hơn.
sehe

Đây là một phiên bản không mã hóa trình lặp cơ sở (vì vậy bạn có thể sử dụng nó với std::insert_iterator<>hoặc std::ostream_iterator<>ví dụ) và cũng cho phép bạn cung cấp một phép chuyển đổi (ví dụ như lambda). c ++ 17, Bắt đầu có vẻ hữu ích / Tương tự trong c ++ 11
tiếp theo

Lưu ý, tại thời điểm này, có rất ít lý do để giữ các trình lặp cơ sở và bạn có thể đơn giản: sử dụng bất kỳ hàm nào , lưu ý rằng Boost chứa một triển khai tốt hơn: boost :: function_output_iterator . Bây giờ tất cả những gì còn lại là phát minh lại for_each_if:)
sehe

Trên thực tế, đọc lại câu hỏi ban đầu, chúng ta hãy thêm tiếng nói của lý do - chỉ sử dụng thư viện chuẩn c ++ 11.
sehe

0
template <class InputIt, class OutputIt, class BinaryOp>
OutputIt
transform_if(InputIt it, InputIt end, OutputIt oit, BinaryOp op)
{
    for(; it != end; ++it, (void) ++oit)
        op(oit, *it);
    return oit;
}

Cách sử dụng: (Lưu ý rằng ĐIỀU KIỆN và CHUYỂN ĐỔI không phải là macro, chúng là các trình giữ chỗ cho bất kỳ điều kiện và phép biến đổi nào bạn muốn áp dụng)

std::vector a{1, 2, 3, 4};
std::vector b;

return transform_if(a.begin(), a.end(), b.begin(),
    [](auto oit, auto item)             // Note the use of 'auto' to make life easier
    {
        if(CONDITION(item))             // Here's the 'if' part
            *oit++ = TRANSFORM(item);   // Here's the 'transform' part
    }
);

bạn đánh giá sản xuất triển khai này đã sẵn sàng chưa? Nó sẽ hoạt động tốt với các phần tử không thể sao chép? Hay các trình vòng lặp di chuyển?
sehe

0

Đây chỉ là câu trả lời cho câu hỏi số 1 "Có cách giải quyết nào tốt hơn với các công cụ thư viện chuẩn C ++ có sẵn không?".

Nếu bạn có thể sử dụng c ++ 17 thì bạn có thể sử dụng std::optionalcho một giải pháp đơn giản hơn chỉ sử dụng chức năng thư viện chuẩn C ++. Ý tưởng là quay lại std::nullopttrong trường hợp không có ánh xạ:

Xem trực tiếp trên Coliru

#include <iostream>
#include <optional>
#include <vector>

template <
    class InputIterator, class OutputIterator, 
    class UnaryOperator
>
OutputIterator filter_transform(InputIterator first1, InputIterator last1,
                            OutputIterator result, UnaryOperator op)
{
    while (first1 != last1) 
    {
        if (auto mapped = op(*first1)) {
            *result = std::move(mapped.value());
            ++result;
        }
        ++first1;
    }
    return result;
}

struct ha { 
    int i;
    explicit ha(int a) : i(a) {}
};

int main()
{
    std::vector<ha> v{ ha{1}, ha{7}, ha{1} }; // initial vector

    // GOAL : make a vector of pointers to elements with i < 2
    std::vector<ha*> ph; // target vector
    filter_transform(v.begin(), v.end(), back_inserter(ph), 
        [](ha &arg) { return arg.i < 2 ? std::make_optional(&arg) : std::nullopt; });

    for (auto p : ph)
        std::cout << p->i << std::endl;

    return 0;
}

Lưu ý rằng tôi vừa triển khai cách tiếp cận của Rust trong C ++ tại đây.


0

Bạn có thể sử dụng thao tác std::accumulatenày hoạt động trên một con trỏ đến vùng chứa đích:

Live On Coliru

#include <numeric>
#include <iostream>
#include <vector>

struct ha
{
    int i;
};

// filter and transform is here
std::vector<int> * fx(std::vector<int> *a, struct ha const & v)
{
    if (v.i < 2)
    {
        a->push_back(v.i);
    }

    return a;
}

int main()
{
    std::vector<ha> v { {1}, {7}, {1}, };

    std::vector<int> ph; // target vector

    std::accumulate(v.begin(), v.end(), &ph, fx);
    
    for (int el : ph)
    {
        std::cout << el << " ";
    }
}

Bản in

1 1 
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.