Toolsets and toolset chains
It's not directly relevant to code in itself, but toolsets affect how one writes and refactors code. There's also the question of how the tools interact with ones own capacities.
I work on Linux/Unix, though usually via Windows workstations in a work setting. I've worked around people who worked, in general, with three different toolsets.Many relatively younger developers use Visual Studio Code, even when working cross-platform. They're part of an IDE generation. I've never really seen the appeal, the few times I've used it. Well, I do, if you don't know what you are doing and need hand-holding.
Most of the rest use vi/vim. If you're a command-line type, this seems to be the editor of choice in the Linux world. I've used vi since, oh, about the mid 1990s or so, but usually as an editor for quick jobs, usually not coding.
I've used CLion on an evaluation basis. Its high degree of integration with cmake makes it an obvious editor if that's how you manage builds, and it has the standard IDE capabilities. Anything I have to say about CLion is about the same as using eclipse for C++, though I'd give CLion the edge if I had to choose between them. At least it's native to Linux.
Personally, I use emacs. It's highly portable - I have used it on DOS, Windows, OS/2, various UNIX flavours, Linux, and VMS. It's fully configurable - at one point I wrote an elaborate text transduction system in emacs lisp. The key combinations are second nature.
Quite aside from the standard c++-mode support, I have extensive custom functions for code generation. Once I'm working on a specific project I usually write a few really customized functions which "know" the shape of the project on disk, or the form of the inheritance trees, or both together.
In most cases the debugger is gdb (In a few cases we got commercial debuggers with GUIs, but for all practical purposes it usually might as well be gdb. (Emacs integrates beautifully with gdb.) In my personal experience a debugger is not really a general-purpose tool: well over half the time tracing output with log statements is quicker and simpler than trying to figure out what is going in in a debugger. The other fraction of the time a debugger is invaluable...
Most employers I have had will not invest in commercial profilers or code coverage tools. This is poor economy. Valgrind and the various other free or open source tools will do a certain amount for you (and for certain sorts of profiling there are excellent tools in, e.g. Richard Sites' book) but simple graphical profilers and code coverage tools lead to much better optimization and testing.
Extending toolchains
The nice thing about emacs is that it's meant to be extensible. And one way to extend it is to take advantage of the Pareto trade-offs: if you want to save keystrokes for yourself, you can just implement the 80 or 90 percent of a task that is easy (but may be wordy) rather than doing all the work for a perfect implementation which usually would require parsing the entire buffer for C++ syntax. It's not the sort of thing that you'd put into a publicly distributed application, but it can still be really useful.As a minor example, consider:
(defun convert-following-function () "Converts from header to file format" (interactive)
(let* ((bpt (point))
(cname (buffer-name))
(is-destructor)
(dot-position (cl-position ?. cname)))
(when dot-position
(setq cname (substring cname 0 dot-position)))
(forward-word 1)
(if (string-equal (buffer-substring bpt (point)) "virtual")
(delete-region bpt (point))
(when (string-equal (buffer-substring bpt (point)) "static")
(delete-region bpt (point))))
(search-forward "(" nil t)
(forward-word -1)
(when (string-equal (buffer-substring (1- (point)) (point)) "~")
(setf is-destructor t)
(forward-char -1))
(insert cname "::")
(search-forward ")" nil t)
(while (not (or (string-equal (buffer-substring (point) (1+ (point))) ";")
(string-equal (buffer-substring (point) (1+ (point))) "{")))
(forward-char 1)
(when (string-equal "override" (buffer-substring (point) (+ 8 (point))))
(delete-region (point) (+ 8 (point)))))
(when (string-equal (buffer-substring (point) (1+ (point))) ";")
(delete-region (point) (1+ (point)))
(if is-destructor
(insert " {}\n")
(insert " {\n\n}\n")))))
This doesn't do much -- it just expands a function declaration into a definition form, trimming inapplicable words and adding a scope name based on the current file. But it takes a few seconds to call, and making those adjustments manually takes rather longer.
For a somewhat more extended example: I make fairly heavy use of the range versions of the STL functions. It takes time to check and write out the proper forms. In addition, I make use of composite views which add another layer of complexity. So I considered writing a function to insert "blank" versions (i.e. with stub names, empty lambdas, and the like) which could then be edited fairly quickly into what was wanted.
I could have done it in emacs lisp, but the full interactive model would have been unwieldy and the code relatively extensive. But emacs can call command-line functions and insert their output into the current buffer. So writing a standalone application which could then be called from emacs is an attractive alternative. In this sense emacs is a full member of the classic UNIX pipeline model: it can act as as a sponge at the end of a pipeline (using compile) or more narrowly invoke one process and incorporate its output directly into a specified buffer.
It's about two thousand lines of go, much of it handling command-line variation. It makes use of go interfaces to extract as much commonality between classes of functions which are lexically similar, and is accordingly rather shorter than an implementation in emacs lisp would have been.
Usage: rgenerate function_name <options>
General options:
-T Use std::views::transform
-F Use std::views::filter
-K Use std::views::keys
-V Use std::views::values
-E Use std::views::elements
-P Use predicate/comparator
-s Use set as target of copying
-v Use vector as target of copying
-p Use projector function(s)
-c Use optional comparator
Specific options:
-IN-M Use iota view (for_each)
-2 Use two-range input (transform)
-w Emulate while(true) with iota view (find_if)
Options which don't apply to the algorithm chosen are simply ignored.
So at the simple end we have
james@imladris:~/src/go/ranges$ ./rgenerate copy
auto [iter1, iter2] = std::ranges::copy(foo, sink);
And at the more complex end we have
james@imladris:~/src/go/ranges$ ./rgenerate transform -2 -v -p
auto [iter1, iter2, iter3] = std::ranges::transform(range1, range2, [&](const foo& inVal, const bar& inVal2) -> baz { },std::back_inserter(sink), [&](const foo& inVal) -> bar { ... return rval; });
Calling it from emacs is straightforward: use tab-completion for the function names and simply prompt for the command-line arguments.
(defun insert-range-stl-template (args) "Inserts a template for an STL range algorithm" (interactive "sArguments: ")
(eval (append '(call-process "~/src/go/ranges/rgenerate" nil t t (completing-read "STL Algorithm: "
'("adjacent_find"
"all_of"
"any_of"
"binary_search"
"count"
"copy"
"copy_backward"
"copy_if"
"copy_n"
"count_if"
"equal"
"equal_range"
"fill"
"fill_n"
"find"
"find_end"
"find_first_of"
"find_if"
"find_if_not"
"find_last"
"find_last_if"
"find_last_if_not"
"for_each"
"for_each_n"
"generate"
"generate_n"
"includes"
"inplace_merge"
"is_heap"
"is_heap_until"
"is_partitioned"
"is_permutation"
"is_sorted"
"is_sorted_until"
"lexicographical_compare"
"lower_bound"
"make_heap"
"max"
"max_element"
"merge"
"min_element"
"min"
"minmax"
"minmax_element"
"mismatch"
"move"
"move_backward"
"next_permutation"
"none_of"
"nth_element"
"partial_sort"
"partial_sort_copy"
"partition"
"partition_copy"
"partition_point"
"prev_permutation"
"push_heap"
"pop_heap"
"remove"
"remove_copy"
"remove_copy_if"
"remove_if"
"replace"
"replace_if"
"replace_copy"
"replace_copy_if"
"reverse"
"reverse_copy"
"rotate"
"rotate_copy"
"sample"
"search"
"search_n"
"set_intersection"
"set_difference"
"set_symmetric_difference"
"set_union"
"shift_left"
"shift_right"
"shuffle"
"sort"
"sort_heap"
"stable_partition"
"stable_sort"
"swap_ranges"
"transform"
"unique"
"unique_copy"
"upper_bound") nil t) ) (split-string args))))
This is a good example of that Pareto trade-off: depending on the user (me) to know what the various flags are rather than providing more extended sets of prompts. A different user might have different requirements, but, so far, this is a private utility. Likewise, the go logic allows the user to do some silly things (use std::views::transform as well as a projection, for example, or to use std::views::filter with std::ranges::copy rather than redirecting to std::ranges::copy_if)*.
There are two special cases which make use of iota views.
We can pass a range of integers and generate a for_each loop over an iota_view:
james@imladris:~/src/go/ranges$ ./rgenerate for_each -I1-19
auto [iter, func] = std::ranges::for_each(std::ranges::iota_view{ 1, 19 }, [&](const int val) { });
We can also pass a -w parameter to find_if and generate a (simple) approximation of a while(true) loop. (A better model is to write a forward iterator interface around the function you will be calling repeatedly until an end condition occurs. The endless iota view isn't really endless -- it will wrap after 2^31 calls and it's not fully idiomatic.)
james@imladris:~/src/go/ranges$ ./rgenerate find_if -w
auto iter = std::ranges::find_if(std::views::iota{1}, [&](const int ref) { if (cond) return true; else return false; });
*There are cases where these make sense, but not in a single call context. If you have a (template) function which internally, in addition to other things, processes an argument using std::ranges::copy, you might very well have a use case where you pass in a compound view such as theVector | std::views::filter(predicateFunction) as an argument.
Comments
Post a Comment